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.
- package/AGENTS.md +167 -0
- package/CHANGELOG.md +465 -430
- package/README.md +604 -548
- package/bin/pi-revit.js +9 -9
- package/docs/architecture.md +271 -0
- package/docs/evaluation.md +434 -0
- package/docs/invariants.json +147 -0
- package/extensions/pi-revit/completion-monitor.ts +55 -0
- package/extensions/pi-revit/contracts.ts +146 -0
- package/extensions/pi-revit/discovery.ts +93 -0
- package/extensions/pi-revit/index.ts +342 -255
- package/extensions/pi-revit/instance-router.ts +86 -86
- package/extensions/pi-revit/platform-prompt.ts +40 -0
- package/extensions/pi-revit/scope-monitor.ts +114 -0
- package/extensions/pi-revit/script-library.ts +144 -144
- package/extensions/pi-revit/tool-catalog.ts +113 -14
- package/extensions/pi-revit/tool-documentation.ts +72 -0
- package/extensions/pi-revit/tool-schema.ts +8 -0
- package/package.json +8 -2
- package/scripts/build.ps1 +9 -9
- package/scripts/check-sdk.ps1 +66 -66
- package/scripts/check-tool-documentation.mjs +287 -0
- package/scripts/deploy.ps1 +16 -16
- package/scripts/generate-contracts.mjs +80 -0
- package/scripts/lib/platform.mjs +226 -0
- package/scripts/test-extension.mjs +15 -0
- package/skills/pi-revit/SKILL.md +30 -218
- package/skills/pi-revit/contracts.generated.json +3524 -0
- package/skills/pi-revit/references/execution-rules.md +41 -0
- package/skills/pi-revit/references/model-audit-export.md +38 -27
- package/skills/pi-revit/references/operation-recovery.md +33 -0
- package/skills/pi-revit/references/room-documentation.md +37 -26
- package/skills/pi-revit/references/tool-index.md +89 -0
- package/skills/pi-revit/references/tools/capture_view.md +62 -0
- package/skills/pi-revit/references/tools/change_element_types.md +65 -0
- package/skills/pi-revit/references/tools/create_tags.md +85 -0
- package/skills/pi-revit/references/tools/delete_elements.md +66 -0
- package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
- package/skills/pi-revit/references/tools/export_documents.md +75 -0
- package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
- package/skills/pi-revit/references/tools/get_element_details.md +66 -0
- package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
- package/skills/pi-revit/references/tools/get_element_types.md +67 -0
- package/skills/pi-revit/references/tools/get_elements.md +87 -0
- package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
- package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
- package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
- package/skills/pi-revit/references/tools/get_model_health.md +53 -0
- package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
- package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
- package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
- package/skills/pi-revit/references/tools/get_schedules.md +71 -0
- package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
- package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
- package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
- package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
- package/skills/pi-revit/references/tools/manage_selection.md +66 -0
- package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
- package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
- package/skills/pi-revit/references/tools/manage_views.md +95 -0
- package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
- package/skills/pi-revit/references/tools/open_view.md +59 -0
- package/skills/pi-revit/references/tools/ping.md +41 -0
- package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
- package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
- package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
- package/skills/pi-revit/references/tools/set_parameters.md +75 -0
- package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
- package/skills/pi-revit/references/tools/transform_elements.md +79 -0
- package/skills/pi-revit/references/visual-verification.md +36 -0
- package/skills/pi-revit/tool-manifest.json +338 -0
- package/src/Revit/BridgeServer.cs +93 -87
- package/src/Revit/OperationStore.cs +178 -178
- package/src/Revit/ToolRegistry.cs +88 -57
- package/src/Revit/Tools/CaptureView.cs +10 -2
- package/src/Revit/Tools/ChangeElementTypes.cs +74 -60
- package/src/Revit/Tools/ChangeSet.cs +39 -0
- package/src/Revit/Tools/CreateTags.cs +107 -95
- package/src/Revit/Tools/DeleteElements.cs +53 -44
- package/src/Revit/Tools/DocumentGuard.cs +74 -64
- package/src/Revit/Tools/ElementNames.cs +103 -0
- package/src/Revit/Tools/ElementQueryScope.cs +27 -27
- package/src/Revit/Tools/ElementTraits.cs +53 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +54 -45
- package/src/Revit/Tools/ExportDocuments.cs +129 -121
- package/src/Revit/Tools/GetElementDetails.cs +37 -41
- package/src/Revit/Tools/GetElementRelationships.cs +82 -76
- package/src/Revit/Tools/GetElementTypes.cs +8 -0
- package/src/Revit/Tools/GetElements.cs +75 -86
- package/src/Revit/Tools/GetLinkedElements.cs +89 -82
- package/src/Revit/Tools/GetLinkedModels.cs +73 -66
- package/src/Revit/Tools/GetModelCoordinates.cs +56 -49
- package/src/Revit/Tools/GetModelHealth.cs +7 -0
- package/src/Revit/Tools/GetModelOverview.cs +185 -158
- package/src/Revit/Tools/GetScheduleFields.cs +44 -37
- package/src/Revit/Tools/GetSchedules.cs +96 -89
- package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
- package/src/Revit/Tools/InheritedState.cs +144 -0
- package/src/Revit/Tools/ManageElementSets.cs +114 -106
- package/src/Revit/Tools/ManageSchedules.cs +174 -164
- package/src/Revit/Tools/ManageSelection.cs +45 -37
- package/src/Revit/Tools/ManageSheetPlacements.cs +113 -97
- package/src/Revit/Tools/ManageSheets.cs +72 -63
- package/src/Revit/Tools/ManageViews.cs +115 -100
- package/src/Revit/Tools/MeasureGeometry.cs +60 -54
- package/src/Revit/Tools/ModelChanges.cs +154 -0
- package/src/Revit/Tools/ModelEditBatch.cs +105 -102
- package/src/Revit/Tools/ModelEditInputs.cs +49 -49
- package/src/Revit/Tools/OpenView.cs +9 -2
- package/src/Revit/Tools/ParameterResolver.cs +94 -0
- package/src/Revit/Tools/QuerySpatialElements.cs +70 -63
- package/src/Revit/Tools/SearchApiDocs.cs +72 -4
- package/src/Revit/Tools/SetParameters.cs +60 -79
- package/src/Revit/Tools/SpatialBounds.cs +30 -30
- package/src/Revit/Tools/SummarizeElements.cs +94 -87
- package/src/Revit/Tools/ToolContract.cs +48 -0
- package/src/Revit/Tools/ToolSupport.cs +5 -1
- package/src/Revit/Tools/TransformElements.cs +72 -57
- 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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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.
|
|
283
|
-
promptGuidelines:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
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
|
|
438
|
-
let
|
|
439
|
-
let
|
|
440
|
-
let
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
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 ||
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
catalog.
|
|
464
|
-
|
|
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
|
-
//
|
|
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
|
+
}
|