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
@@ -1,19 +1,26 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type, type TSchema } from "typebox";
3
- import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
4
- import { randomUUID } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
4
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
5
+ import { randomUUID } from "node:crypto";
5
6
  import os from "node:os";
6
7
  import path from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
- import { version as packageVersion } from "../../package.json";
9
- import { createToolCatalog, type BridgeToolDescriptor } from "./tool-catalog.js";
10
- import { createInstanceRouter, type BridgeInfo } from "./instance-router.js";
11
- import { registerScriptLibrary } from "./script-library.js";
12
-
13
- type BridgeResolver = (operationId?: string) => Promise<BridgeInfo>;
14
-
15
- interface ContentBlock {
16
- type: "text";
9
+ import { version as packageVersion } from "../../package.json";
10
+ import { createToolCatalog, type BridgeToolDescriptor } from "./tool-catalog.js";
11
+ import { createInstanceRouter, type BridgeInfo } from "./instance-router.js";
12
+ import { registerScriptLibrary } from "./script-library.js";
13
+ import { documentationRevision, documentedTools, manualDirectory, skillRoot } from "./tool-documentation.js";
14
+ import { publicBridgeSchema } from "./tool-schema.js";
15
+ import { packagedContracts } from "./contracts.js";
16
+ import { buildPlatformSection, hoistSharedGuidelines } from "./platform-prompt.js";
17
+ import { createCompletionMonitor } from "./completion-monitor.js";
18
+ import { changedModel, createScopeMonitor } from "./scope-monitor.js";
19
+
20
+ type BridgeResolver = (operationId?: string) => Promise<BridgeInfo>;
21
+
22
+ interface ContentBlock {
23
+ type: "text";
17
24
  text: string;
18
25
  }
19
26
 
@@ -31,13 +38,13 @@ interface BridgeToolResponse {
31
38
  const DEFAULT_TIMEOUT_MS = 30_000;
32
39
  const LONG_TIMEOUT_MS = 120_000;
33
40
  const DISCOVERY_TIMEOUT_MS = 10_000;
34
- const MAX_MODEL_CONTENT_CHARS = 12_000;
35
- const MAX_RESULT_PAGE_CHARS = 8_000;
36
-
37
- // IDs only resolve results created by this extension instance. A caller cannot
38
- // turn read_revit_result into an arbitrary filesystem read by supplying a path.
39
- const savedResults = new Map<string, string>();
40
- let resultDirectory: Promise<string> | undefined;
41
+ const MAX_MODEL_CONTENT_CHARS = 12_000;
42
+ const MAX_RESULT_PAGE_CHARS = 8_000;
43
+
44
+ // IDs only resolve results created by this extension instance. A caller cannot
45
+ // turn read_revit_result into an arbitrary filesystem read by supplying a path.
46
+ const savedResults = new Map<string, string>();
47
+ let resultDirectory: Promise<string> | undefined;
41
48
 
42
49
  /** Tools with a longer budget; everything else gets DEFAULT_TIMEOUT_MS. The same
43
50
  * value is sent to the bridge as timeout_ms and used client-side via AbortSignal. */
@@ -96,11 +103,11 @@ function timeoutError(timeoutMs: number): Error {
96
103
 
97
104
  export async function bridgeRequest(
98
105
  pathname: string,
99
- init: { method: "GET" | "POST"; body?: string; query?: Record<string, string>; bridge?: BridgeInfo },
106
+ init: { method: "GET" | "POST"; body?: string; query?: Record<string, string>; bridge?: BridgeInfo },
100
107
  signal?: AbortSignal,
101
108
  timeoutMs = DEFAULT_TIMEOUT_MS,
102
109
  ): Promise<unknown> {
103
- const info = init.bridge ?? await readBridgeInfo();
110
+ const info = init.bridge ?? await readBridgeInfo();
104
111
  const query = new URLSearchParams({ ...(init.query ?? {}), token: info.token });
105
112
  const url = `${info.baseUrl}${pathname}?${query.toString()}`;
106
113
 
@@ -143,147 +150,165 @@ export async function bridgeRequest(
143
150
  return payload;
144
151
  }
145
152
 
146
- export function capText(text: string): string {
147
- if (text.length <= MAX_MODEL_CONTENT_CHARS) return text;
148
- const suffix = `... [truncated preview at ${MAX_MODEL_CONTENT_CHARS} chars]`;
149
- return text.slice(0, Math.max(0, MAX_MODEL_CONTENT_CHARS - suffix.length)) + suffix;
150
- }
151
-
152
- async function modelContent(name: string, payload: BridgeToolResponse): Promise<{ type: "text"; text: string }[]> {
153
- const details = payload.details;
154
- const value = details !== null && typeof details === "object" && Object.hasOwn(details, "payload")
155
- ? (details as { payload: unknown }).payload
156
- : details;
157
- // Current and older bridges both carry the full value in details.payload.
158
- // Pi sends content to the model; details alone is only available to its UI.
159
- const text = details !== undefined
160
- ? JSON.stringify(value, null, 2) ?? "null"
161
- : payload.content?.map((block) => block.text).join("\n") ?? "{}";
162
- if (text.length <= MAX_MODEL_CONTENT_CHARS) return [{ type: "text", text }];
163
-
164
- const resultId = randomUUID();
165
- let filePath: string;
166
- try {
167
- const directory = await (resultDirectory ??= mkdtemp(path.join(os.tmpdir(), "pi-revit-results-")).catch((error) => {
168
- // A transient failure must not poison every later large result in this session.
169
- resultDirectory = undefined;
170
- throw error;
171
- }));
172
- filePath = path.join(directory, `${resultId}.json`);
173
- await writeFile(filePath, text, { encoding: "utf8", flag: "wx", mode: 0o600 });
174
- } catch (error) {
175
- const reason = error instanceof Error ? error.message : String(error);
176
- throw new Error(`Revit completed '${name}', but its large result could not be saved locally: ${reason}. Verify model state before retrying a write.`);
177
- }
178
- savedResults.set(resultId, filePath);
179
- return [{ type: "text", text: JSON.stringify({
180
- result_id: resultId,
181
- file_path: filePath,
182
- total_chars: text.length,
183
- complete_inline: false,
184
- retrieval: { tool: "read_revit_result", result_id: resultId, offset: 0, limit: MAX_RESULT_PAGE_CHARS },
185
- instructions: "The complete result is saved locally. Call read_revit_result, then follow next_offset until has_more is false. Each page is a fragment of the saved text, not a standalone result. Offsets count UTF-16 code units. The absolute file can also be opened with read; it remains available after an extension reload, when this session's result ID may no longer resolve.",
186
- }) }];
187
- }
188
-
189
- function registerResultReader(pi: ExtensionAPI) {
190
- pi.registerTool({
191
- name: "read_revit_result",
192
- label: "Read Saved Revit Result",
193
- description: "Read a bounded fragment of a large Revit tool result using its opaque result_id. This reads a saved local result and does not contact Revit. Follow next_offset until has_more is false; text fragments concatenate to the complete saved result. Offsets count UTF-16 code units.",
194
- parameters: Type.Object({
195
- result_id: Type.String({ description: "Opaque result_id returned by a Revit tool in this extension session." }),
196
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset from the previous page's next_offset; default 0." })),
197
- limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_RESULT_PAGE_CHARS, description: "Maximum characters to return; default 8000. Escaping may require a smaller fragment." })),
198
- }),
199
- executionMode: "sequential",
200
- async execute(_toolCallId, params) {
201
- const offset = params.offset ?? 0;
202
- const limit = params.limit ?? MAX_RESULT_PAGE_CHARS;
203
- if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("offset must be a non-negative integer.");
204
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_RESULT_PAGE_CHARS)
205
- throw new Error(`limit must be an integer from 1 to ${MAX_RESULT_PAGE_CHARS}.`);
206
- const filePath = savedResults.get(params.result_id);
207
- if (!filePath) throw new Error("Unknown result_id for this extension session. Use the original result's file_path with read if the extension was reloaded.");
208
- const text = await readFile(filePath, "utf8");
209
- if (offset > text.length) throw new Error(`offset exceeds this result's ${text.length} characters.`);
210
- const encode = (count: number) => JSON.stringify({
211
- result_id: params.result_id, offset, returned_chars: count, total_chars: text.length,
212
- has_more: offset + count < text.length,
213
- next_offset: offset + count < text.length ? offset + count : null,
214
- fragment: true, text: text.slice(offset, offset + count),
215
- });
216
- // Bound the actual model message, including JSON escaping and metadata.
217
- let low = 0;
218
- let high = Math.min(limit, text.length - offset);
219
- while (low < high) {
220
- const count = Math.ceil((low + high) / 2);
221
- if (encode(count).length <= MAX_MODEL_CONTENT_CHARS) low = count;
222
- else high = count - 1;
223
- }
224
- return { content: [{ type: "text", text: encode(low) }], details: { filePath } };
225
- },
226
- });
227
- }
228
-
229
- async function runBridgeTool(name: string, args: unknown, signal: AbortSignal | undefined, timeoutMs: number, resolve: BridgeResolver,
230
- prepared?: (receipt: { operation_id: string; bridge_id: string }) => Promise<void>) {
231
- const body = { ...(args as Record<string, unknown> ?? {}) };
232
- const retryId = body._operation_id;
233
- delete body._operation_id;
234
- if (retryId !== undefined && (typeof retryId !== "string" || !retryId)) throw new Error("_operation_id must be the exact ID of a previous request.");
235
- const info = await resolve(retryId as string | undefined);
236
- if (retryId && (!info.supportsOperationTracking || !info.bridgeId)) throw new Error("This bridge does not support operation receipts; the request was not sent.");
237
- const operationId = info.supportsOperationTracking && info.bridgeId ? (retryId as string | undefined) ?? `${info.bridgeId}:${randomUUID()}` : undefined;
238
- if (operationId && !operationId.startsWith(`${info.bridgeId}:`)) throw new Error("This operation belongs to a different bridge session. Its outcome is unknown here; it was not replayed.");
239
- if (prepared && (!operationId || !info.bridgeId)) throw new Error("Script library runs require a bridge with operation receipts. The script was not sent.");
240
- try {
241
- if (prepared) await prepared({ operation_id: operationId!, bridge_id: info.bridgeId! });
242
- const payload = (await bridgeRequest(
243
- `/tools/${encodeURIComponent(name)}/execute`,
244
- { method: "POST", body: JSON.stringify(body), bridge: info,
245
- query: { timeout_ms: String(timeoutMs), ...(operationId ? { operation_id: operationId } : {}) } },
246
- signal, timeoutMs,
247
- )) as BridgeToolResponse;
248
- const content = await modelContent(name, payload);
249
- if (operationId) content.push({ type: "text", text: `Operation ID: ${operationId}. Check get_revit_operation after a timeout; retrying with this exact _operation_id and identical arguments will not repeat the action.` });
250
- return { content, details: operationId ? { ...(payload.details as object ?? {}), operation_id: operationId, bridge_id: info.bridgeId } : payload.details };
251
- } catch (error) {
252
- if (!operationId) throw error;
253
- throw new Error(`${error instanceof Error ? error.message : String(error)}\nOperation ID: ${operationId}. Use get_revit_operation to inspect its outcome. Do not retry an edit with a new ID until its effects are known.`);
254
- }
255
- }
256
-
257
- function registerOperationReader(pi: ExtensionAPI, resolve: BridgeResolver) {
258
- pi.registerTool({
259
- name: "get_revit_operation",
260
- label: "Get Revit Operation",
261
- description: "Read an operation receipt without waiting for Revit's model thread. Reports queued, running, succeeded, failed, expired_before_start, result_unavailable or unknown, with the original result when retained. Unknown after restart is not proof that the edit never ran. Full results are bounded to the latest 128 receipts / 32 MiB; IDs remain reserved for up to 10,000 operations per bridge session so expired results never cause re-execution.",
262
- promptSnippet: "Check the outcome of a timed-out Revit operation before retrying an edit.",
263
- parameters: Type.Object({ operation_id: Type.String({ minLength: 1, maxLength: 120 }) }),
264
- executionMode: "sequential",
265
- async execute(_id, args, signal) {
266
- const bridge = await resolve(args.operation_id);
267
- const result = await bridgeRequest(`/operations/${encodeURIComponent(args.operation_id)}`, { method: "GET", bridge }, signal, 10_000);
268
- return { content: await modelContent("get_revit_operation", { details: { payload: result } }), details: result };
269
- },
270
- });
271
- }
272
-
273
- function registerBridgeTool(pi: ExtensionAPI, descriptor: BridgeToolDescriptor, resolve: BridgeResolver) {
274
- const timeoutMs = toolTimeoutMs(descriptor.name);
275
- const schema = structuredClone(descriptor.parameters ?? { type: "object", properties: {} }) as { properties?: Record<string, unknown> };
276
- schema.properties = { ...schema.properties, _operation_id: { type: "string", description: "Optional exact operation ID for retrying an identical earlier request. Reuses its result without repeating the action. Omit for a new operation." } };
153
+ export function capText(text: string): string {
154
+ if (text.length <= MAX_MODEL_CONTENT_CHARS) return text;
155
+ const suffix = `... [truncated preview at ${MAX_MODEL_CONTENT_CHARS} chars]`;
156
+ return text.slice(0, Math.max(0, MAX_MODEL_CONTENT_CHARS - suffix.length)) + suffix;
157
+ }
158
+
159
+ async function modelContent(name: string, payload: BridgeToolResponse): Promise<{ type: "text"; text: string }[]> {
160
+ const details = payload.details;
161
+ const value = details !== null && typeof details === "object" && Object.hasOwn(details, "payload")
162
+ ? (details as { payload: unknown }).payload
163
+ : details;
164
+ // Current and older bridges both carry the full value in details.payload.
165
+ // Pi sends content to the model; details alone is only available to its UI.
166
+ const text = details !== undefined
167
+ ? JSON.stringify(value, null, 2) ?? "null"
168
+ : payload.content?.map((block) => block.text).join("\n") ?? "{}";
169
+ if (text.length <= MAX_MODEL_CONTENT_CHARS) return [{ type: "text", text }];
170
+
171
+ const resultId = randomUUID();
172
+ let filePath: string;
173
+ try {
174
+ const directory = await (resultDirectory ??= mkdtemp(path.join(os.tmpdir(), "pi-revit-results-")).catch((error) => {
175
+ // A transient failure must not poison every later large result in this session.
176
+ resultDirectory = undefined;
177
+ throw error;
178
+ }));
179
+ filePath = path.join(directory, `${resultId}.json`);
180
+ await writeFile(filePath, text, { encoding: "utf8", flag: "wx", mode: 0o600 });
181
+ } catch (error) {
182
+ const reason = error instanceof Error ? error.message : String(error);
183
+ throw new Error(`Revit completed '${name}', but its large result could not be saved locally: ${reason}. Verify model state before retrying a write.`);
184
+ }
185
+ savedResults.set(resultId, filePath);
186
+ return [{ type: "text", text: JSON.stringify({
187
+ result_id: resultId,
188
+ file_path: filePath,
189
+ total_chars: text.length,
190
+ complete_inline: false,
191
+ retrieval: { tool: "read_revit_result", result_id: resultId, offset: 0, limit: MAX_RESULT_PAGE_CHARS },
192
+ instructions: "The complete result is saved locally. Call read_revit_result, then follow next_offset until has_more is false. Each page is a fragment of the saved text, not a standalone result. Offsets count UTF-16 code units. The absolute file can also be opened with read; it remains available after an extension reload, when this session's result ID may no longer resolve.",
193
+ }) }];
194
+ }
195
+
196
+ function registerResultReader(pi: ExtensionAPI) {
197
+ pi.registerTool({
198
+ name: "read_revit_result",
199
+ label: "Read Saved Revit Result",
200
+ description: "Read a bounded fragment of a large Revit tool result using its opaque result_id. This reads a saved local result and does not contact Revit. Follow next_offset until has_more is false; text fragments concatenate to the complete saved result. Offsets count UTF-16 code units.",
201
+ parameters: Type.Object({
202
+ result_id: Type.String({ description: "Opaque result_id returned by a Revit tool in this extension session." }),
203
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset from the previous page's next_offset; default 0." })),
204
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_RESULT_PAGE_CHARS, description: "Maximum characters to return; default 8000. Escaping may require a smaller fragment." })),
205
+ }),
206
+ executionMode: "sequential",
207
+ async execute(_toolCallId, params) {
208
+ const offset = params.offset ?? 0;
209
+ const limit = params.limit ?? MAX_RESULT_PAGE_CHARS;
210
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("offset must be a non-negative integer.");
211
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_RESULT_PAGE_CHARS)
212
+ throw new Error(`limit must be an integer from 1 to ${MAX_RESULT_PAGE_CHARS}.`);
213
+ const filePath = savedResults.get(params.result_id);
214
+ if (!filePath) throw new Error("Unknown result_id for this extension session. Use the original result's file_path with read if the extension was reloaded.");
215
+ const text = await readFile(filePath, "utf8");
216
+ if (offset > text.length) throw new Error(`offset exceeds this result's ${text.length} characters.`);
217
+ const encode = (count: number) => JSON.stringify({
218
+ result_id: params.result_id, offset, returned_chars: count, total_chars: text.length,
219
+ has_more: offset + count < text.length,
220
+ next_offset: offset + count < text.length ? offset + count : null,
221
+ fragment: true, text: text.slice(offset, offset + count),
222
+ });
223
+ // Bound the actual model message, including JSON escaping and metadata.
224
+ let low = 0;
225
+ let high = Math.min(limit, text.length - offset);
226
+ while (low < high) {
227
+ const count = Math.ceil((low + high) / 2);
228
+ if (encode(count).length <= MAX_MODEL_CONTENT_CHARS) low = count;
229
+ else high = count - 1;
230
+ }
231
+ return { content: [{ type: "text", text: encode(low) }], details: { filePath } };
232
+ },
233
+ });
234
+ }
235
+
236
+ async function runBridgeTool(name: string, args: unknown, signal: AbortSignal | undefined, timeoutMs: number, resolve: BridgeResolver,
237
+ prepared?: (receipt: { operation_id: string; bridge_id: string }) => Promise<void>) {
238
+ const body = { ...(args as Record<string, unknown> ?? {}) };
239
+ const retryId = body._operation_id;
240
+ delete body._operation_id;
241
+ if (retryId !== undefined && (typeof retryId !== "string" || !retryId)) throw new Error("_operation_id must be the exact ID of a previous request.");
242
+ const info = await resolve(retryId as string | undefined);
243
+ if (retryId && (!info.supportsOperationTracking || !info.bridgeId)) throw new Error("This bridge does not support operation receipts; the request was not sent.");
244
+ const operationId = info.supportsOperationTracking && info.bridgeId ? (retryId as string | undefined) ?? `${info.bridgeId}:${randomUUID()}` : undefined;
245
+ if (operationId && !operationId.startsWith(`${info.bridgeId}:`)) throw new Error("This operation belongs to a different bridge session. Its outcome is unknown here; it was not replayed.");
246
+ if (prepared && (!operationId || !info.bridgeId)) throw new Error("Script library runs require a bridge with operation receipts. The script was not sent.");
247
+ try {
248
+ if (prepared) await prepared({ operation_id: operationId!, bridge_id: info.bridgeId! });
249
+ const payload = (await bridgeRequest(
250
+ `/tools/${encodeURIComponent(name)}/execute`,
251
+ { method: "POST", body: JSON.stringify(body), bridge: info,
252
+ query: { timeout_ms: String(timeoutMs), ...(operationId ? { operation_id: operationId } : {}) } },
253
+ signal, timeoutMs,
254
+ )) as BridgeToolResponse;
255
+ const content = await modelContent(name, payload);
256
+ if (operationId) content.push({ type: "text", text: `Operation ID: ${operationId}. Check get_revit_operation after a timeout; retrying with this exact _operation_id and identical arguments will not repeat the action.` });
257
+ return { content, details: operationId ? { ...(payload.details as object ?? {}), operation_id: operationId, bridge_id: info.bridgeId } : payload.details };
258
+ } catch (error) {
259
+ if (!operationId) throw error;
260
+ throw new Error(`${error instanceof Error ? error.message : String(error)}\nOperation ID: ${operationId}. Use get_revit_operation to inspect its outcome. Do not retry an edit with a new ID until its effects are known.`);
261
+ }
262
+ }
263
+
264
+ function registerOperationReader(pi: ExtensionAPI, resolve: BridgeResolver) {
265
+ pi.registerTool({
266
+ name: "get_revit_operation",
267
+ label: "Get Revit Operation",
268
+ description: "Read an operation receipt without waiting for Revit's model thread. Reports queued, running, succeeded, failed, expired_before_start, result_unavailable or unknown, with the original result when retained. Unknown after restart is not proof that the edit never ran. Full results are bounded to the latest 128 receipts / 32 MiB; IDs remain reserved for up to 10,000 operations per bridge session so expired results never cause re-execution.",
269
+ promptSnippet: "Check the outcome of a timed-out Revit operation before retrying an edit.",
270
+ parameters: Type.Object({ operation_id: Type.String({ minLength: 1, maxLength: 120 }) }),
271
+ executionMode: "sequential",
272
+ async execute(_id, args, signal) {
273
+ const bridge = await resolve(args.operation_id);
274
+ const result = await bridgeRequest(`/operations/${encodeURIComponent(args.operation_id)}`, { method: "GET", bridge }, signal, 10_000);
275
+ return { content: await modelContent("get_revit_operation", { details: { payload: result } }), details: result };
276
+ },
277
+ });
278
+ }
279
+
280
+ interface RequestMonitors { completion: ReturnType<typeof createCompletionMonitor>; scope: ReturnType<typeof createScopeMonitor> }
281
+
282
+ /** The bridge value of a tool result (details.payload for current and older bridges). */
283
+ function resultPayload(details: unknown): unknown {
284
+ return details !== null && typeof details === "object" && Object.hasOwn(details, "payload") ? (details as { payload: unknown }).payload : details;
285
+ }
286
+
287
+ /** Notes the request monitors add to a result: scope first (it can require asking the user), then completion. */
288
+ function monitorNotes(monitors: RequestMonitors | undefined, tool: string, params: unknown, meta: { write?: boolean; effects?: string[] | null }, details: unknown): string[] {
289
+ if (!monitors) return [];
290
+ const payload = resultPayload(details);
291
+ return [monitors.scope.afterResult(payload), monitors.completion.afterCall(tool, params, meta, changedModel(payload))].filter((note): note is string => !!note);
292
+ }
293
+
294
+ /** `guidelines` are this tool's own rules; rules shared by several tools live once in the platform section. */
295
+ function registerBridgeTool(pi: ExtensionAPI, descriptor: BridgeToolDescriptor, resolve: BridgeResolver, guidelines: string[],
296
+ monitors?: RequestMonitors) {
297
+ const timeoutMs = toolTimeoutMs(descriptor.name);
298
+ const schema = publicBridgeSchema(descriptor.parameters);
277
299
  pi.registerTool({
278
300
  name: descriptor.name,
279
301
  label: descriptor.label ?? descriptor.name,
280
302
  description: descriptor.description ?? `Revit bridge tool '${descriptor.name}'.`,
281
- parameters: Type.Unsafe(schema as TSchema),
282
- promptSnippet: descriptor.tier === "advanced" ? undefined : descriptor.promptSnippet ?? undefined,
283
- promptGuidelines: descriptor.tier === "advanced" ? undefined : descriptor.promptGuidelines ?? undefined,
303
+ parameters: Type.Unsafe(schema as TSchema),
304
+ promptSnippet: descriptor.promptSnippet ?? undefined,
305
+ promptGuidelines: guidelines,
284
306
  executionMode: descriptor.executionMode === "parallel" ? "parallel" : "sequential",
285
307
  async execute(_toolCallId, params, signal) {
286
- return runBridgeTool(descriptor.name, params, signal, timeoutMs, resolve);
308
+ const result = await runBridgeTool(descriptor.name, params, signal, timeoutMs, resolve);
309
+ // Result- and metadata-driven steering: objects that predate the request, and repeated re-verification after edits.
310
+ const notes = monitorNotes(monitors, descriptor.name, params, descriptor, result.details);
311
+ return notes.length ? { ...result, content: [...result.content, ...notes.map(text => ({ type: "text" as const, text }))] } : result;
287
312
  },
288
313
  });
289
314
  }
@@ -366,17 +391,51 @@ async function announceUpdateOnce(notify: (message: string, level: "info") => vo
366
391
  }
367
392
  }
368
393
 
369
- function registerPing(pi: ExtensionAPI, resolve: BridgeResolver, onBridgeAlive?: () => Promise<"ready" | "registered" | "failed">) {
394
+ const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
395
+ const nativeToolNames = new Set(documentedTools.filter(tool => tool.source === "native").map(tool => tool.name));
396
+
397
+ /** Branch and commit when the extension runs from a source checkout; null for an installed package. */
398
+ function sourceRevision(): { branch: string | null; commit: string | null } | null {
399
+ try {
400
+ const gitDir = path.join(packageRoot, ".git");
401
+ const head = readFileSync(path.join(gitDir, "HEAD"), "utf8").trim();
402
+ if (!head.startsWith("ref: ")) return { branch: null, commit: head };
403
+ const ref = head.slice(5);
404
+ let commit: string | null = null;
405
+ try { commit = readFileSync(path.join(gitDir, ref), "utf8").trim(); }
406
+ catch { commit = readFileSync(path.join(gitDir, "packed-refs"), "utf8").split(/\r?\n/).find(line => line.endsWith(` ${ref}`))?.split(" ")[0] ?? null; }
407
+ return { branch: ref.replace(/^refs\/heads\//, ""), commit };
408
+ } catch { return null; }
409
+ }
410
+
411
+ /** What is actually loaded: package, guidance revision, source revision and per-tool contract agreement. */
412
+ function loadedStatus(live: Map<string, string>) {
413
+ const matching: string[] = [], changed: string[] = [], undocumented: string[] = [];
414
+ for (const [name, hash] of live) {
415
+ const packaged = packagedContracts.get(name);
416
+ if (!packaged) undocumented.push(name);
417
+ else if (packaged.contract_hash === hash) matching.push(name);
418
+ else changed.push(name);
419
+ }
420
+ return {
421
+ extension_package: packageVersion, extension_root: packageRoot, documentation_revision: documentationRevision, source: sourceRevision(),
422
+ contracts: live.size ? { matching: matching.length, changed, undocumented,
423
+ packaged_not_advertised: [...packagedContracts.values()].filter(tool => tool.source === "bridge" && !live.has(tool.name)).map(tool => tool.name) }
424
+ : "no bridge catalogue discovered yet",
425
+ };
426
+ }
427
+
428
+ function registerPing(pi: ExtensionAPI, resolve: BridgeResolver, onBridgeAlive?: () => Promise<"ready" | "registered" | "failed">, status?: () => unknown) {
370
429
  pi.registerTool({
371
430
  name: "ping",
372
431
  label: "Ping Revit Bridge",
373
- description: "Check that the Revit bridge is reachable and report the Revit version.",
432
+ description: "Check that the Revit bridge is reachable and report the Revit version, plus which PI-Revit extension package, guidance revision, source revision and tool contracts are loaded (changed or undocumented contracts mean manuals may not match the bridge).",
374
433
  parameters: Type.Object({}),
375
434
  promptSnippet: "Check Revit bridge availability.",
376
- promptGuidelines: ["Use ping when Revit tools fail or bridge availability is unclear."],
435
+ promptGuidelines: ["Use ping when Revit tools fail or bridge availability is unclear; it also reports which PI-Revit package, guidance revision and contracts are loaded."],
377
436
  executionMode: "sequential",
378
437
  async execute(_toolCallId, _params, signal) {
379
- const payload = await bridgeRequest("/ping", { method: "GET", bridge: await resolve() }, signal, 10_000);
438
+ const payload = await bridgeRequest("/ping", { method: "GET", bridge: await resolve() }, signal, 10_000);
380
439
  const warning = versionMismatch((payload as { addinVersion?: string }).addinVersion);
381
440
  // The bridge is alive: if this session started before Revit and only has
382
441
  // ping, register the bridge tools now and tell the model they arrived.
@@ -388,9 +447,11 @@ function registerPing(pi: ExtensionAPI, resolve: BridgeResolver, onBridgeAlive?:
388
447
  else if (state === "failed")
389
448
  registrationNote = "\nNOTE: Bridge tool discovery failed even though ping succeeded; retry ping or restart pi.";
390
449
  }
450
+ const loaded = status?.();
391
451
  return {
392
- content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") + registrationNote }],
393
- details: payload,
452
+ content: [{ type: "text", text: JSON.stringify(payload) + (warning ? `\nWARNING: ${warning}` : "") + registrationNote
453
+ + (loaded ? `\nPI-Revit loaded: ${JSON.stringify(loaded)}` : "") }],
454
+ details: loaded ? { ...(payload as object), pi_revit: loaded } : payload,
394
455
  };
395
456
  },
396
457
  });
@@ -398,76 +459,93 @@ function registerPing(pi: ExtensionAPI, resolve: BridgeResolver, onBridgeAlive?:
398
459
 
399
460
  const REDISCOVERY_INTERVAL_MS = 15_000;
400
461
 
401
- export default async function revitConnector(pi: ExtensionAPI) {
402
- const instances = createInstanceRouter(readBridgeInfo, async info => await bridgeRequest("/ping", { method: "GET", bridge: info }, undefined, 2000) as Record<string, unknown>);
403
- registerResultReader(pi);
404
- registerOperationReader(pi, instances.resolve);
405
- registerScriptLibrary(pi, (args, signal, prepared) => runBridgeTool("execute_csharp", args, signal, LONG_TIMEOUT_MS, instances.resolve, prepared),
406
- async value => ({ content: await modelContent("manage_revit_scripts", { details: { payload: value } }), details: value }));
407
- pi.registerTool({
408
- name: "manage_revit_instances", label: "Manage Revit Instances",
409
- description: "List reachable local Revit bridge sessions or select one for this Pi session. The first sole instance is bound automatically; multiple instances require explicit selection before model calls. After that session closes or restarts, select its new bridge_id: calls never fall back to another session. Selection refreshes the tool catalogue. Operation receipt lookups and identical retries use their original session. Read get_model_overview again after switching; document IDs are session-specific.",
410
- parameters: Type.Object({ action: Type.Optional(Type.Union([Type.Literal("list"), Type.Literal("select")])), bridge_id: Type.Optional(Type.String()) }),
411
- executionMode: "sequential",
412
- async execute(_id, args) {
413
- let result: unknown;
414
- if ((args.action ?? "list") === "list") result = { instances: await instances.list() };
415
- else if (args.action === "select" && args.bridge_id) {
416
- // Drain discovery for the previous target before changing selection.
417
- if (discoveryInFlight) await discoveryInFlight;
418
- const selection = await instances.select(args.bridge_id);
419
- // A retry timer may have started another discovery while selection probed.
420
- // Drain that request too, then reset synchronously before fetching anew.
421
- if (discoveryInFlight) await discoveryInFlight;
422
- bridgeToolsRegistered = false;
423
- catalog.reset();
424
- const ready = await discoverAndRegister();
425
- result = { ...selection, tool_catalog_ready: ready };
426
- if (!ready && sessionActive) startRetry();
427
- } else throw new Error("select requires bridge_id from the instance list.");
428
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
429
- },
430
- });
462
+ export default async function revitConnector(pi: ExtensionAPI) {
463
+ const bridgeVersions = new Map<string, string | null>();
464
+ const bridgeKey = (info: BridgeInfo) => info.bridgeId ?? `${info.baseUrl}\0${info.token}`;
465
+ const instances = createInstanceRouter(readBridgeInfo, async info => {
466
+ const payload = await bridgeRequest("/ping", { method: "GET", bridge: info }, undefined, 2000) as Record<string, unknown>;
467
+ bridgeVersions.set(bridgeKey(info), typeof payload.addinVersion === "string" ? payload.addinVersion : null);
468
+ return payload;
469
+ });
470
+ registerResultReader(pi);
471
+ registerOperationReader(pi, instances.resolve);
472
+ const monitors: RequestMonitors = { completion: createCompletionMonitor(), scope: createScopeMonitor() };
473
+ registerScriptLibrary(pi, async (args, signal, prepared) => {
474
+ const result = await runBridgeTool("execute_csharp", args, signal, LONG_TIMEOUT_MS, instances.resolve, prepared);
475
+ const notes = monitorNotes(monitors, "manage_revit_scripts", args, { write: true }, result.details);
476
+ return notes.length ? { ...result, content: [...result.content, ...notes.map(text => ({ type: "text" as const, text }))] } : result;
477
+ },
478
+ async value => ({ content: await modelContent("manage_revit_scripts", { details: { payload: value } }), details: value }));
479
+ pi.registerTool({
480
+ name: "manage_revit_instances", label: "Manage Revit Instances",
481
+ description: "List reachable local Revit bridge sessions or select one for this Pi session. The first sole instance is bound automatically; multiple instances require explicit selection before model calls. After that session closes or restarts, select its new bridge_id: calls never fall back to another session. Selection refreshes the tool catalogue. Operation receipt lookups and identical retries use their original session. Read get_model_overview again after switching; document IDs are session-specific.",
482
+ parameters: Type.Object({ action: Type.Optional(Type.Union([Type.Literal("list"), Type.Literal("select")])), bridge_id: Type.Optional(Type.String()) }),
483
+ executionMode: "sequential",
484
+ async execute(_id, args) {
485
+ let result: unknown;
486
+ if ((args.action ?? "list") === "list") result = { instances: await instances.list() };
487
+ else if (args.action === "select" && args.bridge_id) {
488
+ // Drain discovery for the previous target before changing selection.
489
+ if (discoveryInFlight) await discoveryInFlight;
490
+ const selection = await instances.select(args.bridge_id);
491
+ // A retry timer may have started another discovery while selection probed.
492
+ // Drain that request too, then reset synchronously before fetching anew.
493
+ if (discoveryInFlight) await discoveryInFlight;
494
+ bridgeToolsRegistered = false;
495
+ catalog.reset();
496
+ const ready = await discoverAndRegister();
497
+ result = { ...selection, tool_catalog_ready: ready };
498
+ if (!ready && sessionActive) startRetry();
499
+ } else throw new Error("select requires bridge_id from the instance list.");
500
+ return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
501
+ },
502
+ });
431
503
  // Self-healing discovery: when pi starts before Revit is ready, the initial
432
- // GET /tools fails and only ping is registered. Rather than requiring a
504
+ // GET /tools fails and only native utilities are registered. Rather than requiring a
433
505
  // fresh pi start (/reload does not reliably re-run async registration), a
434
506
  // background retry keeps probing until the bridge appears, and a successful
435
507
  // ping also triggers an immediate attempt.
436
- let bridgeToolsRegistered = false;
437
- let discoveryInFlight: Promise<boolean> | null = null;
438
- let sessionActive = false;
439
- let disposed = false;
440
- let timer: ReturnType<typeof setInterval> | undefined;
441
- const catalog = createToolCatalog(pi, discoverAndRegister);
442
-
443
- async function discoverAndRegister(): Promise<boolean> {
444
- if (disposed) return false;
508
+ let bridgeToolsRegistered = false;
509
+ let sharedRules: string[] = [];
510
+ let discoveryInFlight: Promise<boolean> | null = null;
511
+ let sessionActive = false;
512
+ let disposed = false;
513
+ let timer: ReturnType<typeof setInterval> | undefined;
514
+ const catalog = createToolCatalog(pi, discoverAndRegister);
515
+
516
+ async function discoverAndRegister(): Promise<boolean> {
517
+ if (disposed) return false;
445
518
  if (bridgeToolsRegistered) return true;
446
519
  if (discoveryInFlight) return discoveryInFlight;
447
520
  discoveryInFlight = (async () => {
448
521
  try {
449
- const payload = (await bridgeRequest("/tools", { method: "GET", bridge: await instances.resolve() }, undefined, DISCOVERY_TIMEOUT_MS)) as {
522
+ const selectedBridge = await instances.resolve();
523
+ const payload = (await bridgeRequest("/tools", { method: "GET", bridge: selectedBridge }, undefined, DISCOVERY_TIMEOUT_MS)) as {
450
524
  tools?: BridgeToolDescriptor[];
451
525
  };
452
- const descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
453
- if (disposed || descriptors.length === 0) return false;
454
- const added: string[] = [];
455
- for (const descriptor of descriptors) {
456
- if (!descriptor || typeof descriptor.name !== "string" || !descriptor.name) continue;
457
- if (["ping", "read_revit_result", "find_revit_tools", "get_revit_operation", "manage_revit_instances", "manage_revit_scripts"].includes(descriptor.name)) continue;
458
- registerBridgeTool(pi, descriptor, instances.resolve);
459
- catalog.add(descriptor);
460
- added.push(descriptor.name);
461
- }
462
- if (sessionActive) {
463
- catalog.hideAdvanced(added);
464
- pi.setActiveTools([...new Set([...pi.getActiveTools(), ...descriptors.filter(d => added.includes(d.name) && d.tier !== "advanced").map(d => d.name)])]);
465
- }
526
+ const descriptors = Array.isArray(payload?.tools) ? payload.tools : [];
527
+ if (disposed || !Array.isArray(payload?.tools)) return false;
528
+ catalog.setBridgeVersion(bridgeVersions.get(bridgeKey(selectedBridge)) ?? null);
529
+ // Native tool names are reserved: a bridge cannot replace an extension utility.
530
+ const valid = descriptors.filter(descriptor => descriptor && typeof descriptor.name === "string" && descriptor.name
531
+ && !nativeToolNames.has(descriptor.name));
532
+ const { shared, perTool } = hoistSharedGuidelines(valid);
533
+ sharedRules = shared;
534
+ const added: string[] = [];
535
+ for (const descriptor of valid) {
536
+ registerBridgeTool(pi, descriptor, instances.resolve, perTool.get(descriptor.name) ?? [], monitors);
537
+ catalog.add(descriptor);
538
+ added.push(descriptor.name);
539
+ }
540
+ if (sessionActive) {
541
+ catalog.hideAdvanced(added);
542
+ pi.setActiveTools([...new Set([...pi.getActiveTools(), ...descriptors.filter(d => added.includes(d.name) && d.tier !== "advanced").map(d => d.name)])]);
543
+ }
466
544
  bridgeToolsRegistered = true;
467
545
  return true;
468
546
  } catch {
469
547
  // Bridge down (Revit closed, still starting, stale bridge.json):
470
- // stay on ping only and try again later.
548
+ // keep native utilities available and try again later.
471
549
  return false;
472
550
  } finally {
473
551
  discoveryInFlight = null;
@@ -480,21 +558,30 @@ export default async function revitConnector(pi: ExtensionAPI) {
480
558
  // bridge is down, so it is never part of /tools discovery. A successful
481
559
  // ping doubles as a re-discovery trigger — the natural first call in a
482
560
  // session that finds itself without bridge tools.
483
- registerPing(pi, instances.resolve, async () => {
561
+ registerPing(pi, instances.resolve, async () => {
484
562
  if (bridgeToolsRegistered) return "ready";
485
563
  return (await discoverAndRegister()) ? "registered" : "failed";
564
+ }, () => loadedStatus(catalog.liveContracts()));
565
+
566
+ // One always-visible platform section per run: global protocols plus rules shared
567
+ // by several tools. It does not depend on the skill being read or on tool count.
568
+ pi.on("before_agent_start", async event => {
569
+ monitors.completion.reset();
570
+ monitors.scope.reset((event as { prompt?: string }).prompt ?? "");
571
+ const options = (event as { systemPromptOptions?: { sections?: Record<string, string> } }).systemPromptOptions;
572
+ if (options?.sections) options.sections.pi_revit = buildPlatformSection({ manualDirectory, skillRoot, sharedRules });
486
573
  });
487
574
 
488
575
  // Surface an incomplete update (see versionMismatch) once per session, right
489
576
  // where the user lands after running `pi update --extensions`. Bridge down at
490
577
  // session start is the normal Revit-closed case: stay quiet.
491
- pi.on("session_start", async (_event, ctx) => {
492
- sessionActive = true;
493
- catalog.hideAdvanced();
494
- if (!bridgeToolsRegistered) startRetry();
578
+ pi.on("session_start", async (_event, ctx) => {
579
+ sessionActive = true;
580
+ catalog.hideAdvanced();
581
+ if (!bridgeToolsRegistered) startRetry();
495
582
  await announceUpdateOnce((message, level) => ctx.ui.notify(message, level));
496
583
  try {
497
- const payload = (await bridgeRequest("/ping", { method: "GET", bridge: await instances.resolve() }, undefined, 3_000)) as { addinVersion?: string };
584
+ const payload = (await bridgeRequest("/ping", { method: "GET", bridge: await instances.resolve() }, undefined, 3_000)) as { addinVersion?: string };
498
585
  const warning = versionMismatch(payload.addinVersion);
499
586
  if (warning) ctx.ui.notify(warning, "warning");
500
587
  } catch {
@@ -502,35 +589,35 @@ export default async function revitConnector(pi: ExtensionAPI) {
502
589
  }
503
590
  });
504
591
 
505
- pi.on("session_shutdown", async () => {
506
- disposed = true;
507
- sessionActive = false;
508
- if (timer) clearInterval(timer);
509
- timer = undefined;
510
- });
511
-
512
- function startRetry() {
513
- if (timer || disposed) return;
514
- timer = setInterval(async () => {
515
- if (!(await discoverAndRegister())) return;
516
- if (timer) clearInterval(timer);
517
- timer = undefined;
518
- if (disposed) return;
519
- // Refresh the model's knowledge on its next turn without interrupting the user.
520
- try {
521
- pi.sendMessage(
522
- {
523
- customType: "pi-revit",
524
- content: "Revit is now reachable. Core bridge tools are available; use find_revit_tools to activate specialist tools.",
525
- display: true,
526
- },
527
- { deliverAs: "nextTurn" },
528
- );
529
- } catch {
530
- // Tool registration remains valid if the session cannot accept a message.
531
- }
532
- }, REDISCOVERY_INTERVAL_MS);
533
- timer.unref?.();
534
- }
535
- await discoverAndRegister();
536
- }
592
+ pi.on("session_shutdown", async () => {
593
+ disposed = true;
594
+ sessionActive = false;
595
+ if (timer) clearInterval(timer);
596
+ timer = undefined;
597
+ });
598
+
599
+ function startRetry() {
600
+ if (timer || disposed) return;
601
+ timer = setInterval(async () => {
602
+ if (!(await discoverAndRegister())) return;
603
+ if (timer) clearInterval(timer);
604
+ timer = undefined;
605
+ if (disposed) return;
606
+ // Refresh the model's knowledge on its next turn without interrupting the user.
607
+ try {
608
+ pi.sendMessage(
609
+ {
610
+ customType: "pi-revit",
611
+ content: "Revit is now reachable. Core bridge tools are available; use find_revit_tools to activate specialist tools.",
612
+ display: true,
613
+ },
614
+ { deliverAs: "nextTurn" },
615
+ );
616
+ } catch {
617
+ // Tool registration remains valid if the session cannot accept a message.
618
+ }
619
+ }, REDISCOVERY_INTERVAL_MS);
620
+ timer.unref?.();
621
+ }
622
+ await discoverAndRegister();
623
+ }