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,178 +1,178 @@
|
|
|
1
|
-
using System.Diagnostics;
|
|
2
|
-
using System.Security.Cryptography;
|
|
3
|
-
using System.Text.Json;
|
|
4
|
-
|
|
5
|
-
namespace RevitBridge;
|
|
6
|
-
|
|
7
|
-
/// <summary>Session-local operation receipts. Expiring a result never permits the same ID to execute again.</summary>
|
|
8
|
-
internal sealed class OperationStore
|
|
9
|
-
{
|
|
10
|
-
internal sealed record Reply(int Status, object Response, string? Outcome = null);
|
|
11
|
-
internal sealed class Entry
|
|
12
|
-
{
|
|
13
|
-
internal required string Id;
|
|
14
|
-
internal required string Tool;
|
|
15
|
-
internal required string Fingerprint;
|
|
16
|
-
internal required long Deadline;
|
|
17
|
-
internal DateTimeOffset Created = DateTimeOffset.UtcNow;
|
|
18
|
-
internal DateTimeOffset? Started;
|
|
19
|
-
internal DateTimeOffset? Finished;
|
|
20
|
-
internal string State = "queued";
|
|
21
|
-
internal TaskCompletionSource<Reply>? Completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
22
|
-
internal Reply? SavedReply;
|
|
23
|
-
internal int SavedBytes;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private readonly object _gate = new();
|
|
27
|
-
private readonly Dictionary<string, Entry> _entries = new(StringComparer.Ordinal);
|
|
28
|
-
private readonly Queue<Entry> _results = new();
|
|
29
|
-
private readonly int _maxRecords;
|
|
30
|
-
private readonly int _maxResults;
|
|
31
|
-
private readonly int _maxBytes;
|
|
32
|
-
private int _bytes;
|
|
33
|
-
public string Generation { get; } = Guid.NewGuid().ToString("N");
|
|
34
|
-
|
|
35
|
-
internal OperationStore(int maxRecords = 10000, int maxResults = 128, int maxBytes = 32 * 1024 * 1024)
|
|
36
|
-
{
|
|
37
|
-
_maxRecords = maxRecords; _maxResults = maxResults; _maxBytes = maxBytes;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
public (Entry Entry, bool IsNew) Reserve(string id, string tool, JsonElement args, TimeSpan timeout)
|
|
41
|
-
{
|
|
42
|
-
id = ValidateId(id);
|
|
43
|
-
string fingerprint = Fingerprint(tool, args);
|
|
44
|
-
lock (_gate)
|
|
45
|
-
{
|
|
46
|
-
if (_entries.TryGetValue(id, out var existing))
|
|
47
|
-
{
|
|
48
|
-
if (existing.Fingerprint != fingerprint) throw new ArgumentException("operation_id was already used with different tool arguments. No new operation was started.");
|
|
49
|
-
return (existing, false);
|
|
50
|
-
}
|
|
51
|
-
if (_entries.Count >= _maxRecords) throw new InvalidOperationException("Operation receipt capacity reached. Restart the bridge before starting new tracked operations; existing receipts remain queryable.");
|
|
52
|
-
var entry = new Entry { Id = id, Tool = tool, Fingerprint = fingerprint,
|
|
53
|
-
Deadline = Stopwatch.GetTimestamp() + (long)(timeout.TotalSeconds * Stopwatch.Frequency) };
|
|
54
|
-
_entries.Add(id, entry);
|
|
55
|
-
return (entry, true);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
public Task<Reply> Wait(Entry entry)
|
|
60
|
-
{
|
|
61
|
-
lock (_gate)
|
|
62
|
-
{
|
|
63
|
-
ExpireQueued(entry);
|
|
64
|
-
return entry.Completion?.Task ?? Task.FromResult(entry.SavedReply ?? new Reply(409, new
|
|
65
|
-
{
|
|
66
|
-
error = true, operation_id = entry.Id, state = entry.State,
|
|
67
|
-
message = "The operation already finished, but its full result has expired. It was not repeated. Inspect current model state before deciding on a new operation.",
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
public bool TryStart(Entry entry)
|
|
73
|
-
{
|
|
74
|
-
lock (_gate)
|
|
75
|
-
{
|
|
76
|
-
ExpireQueued(entry);
|
|
77
|
-
if (entry.State != "queued") return false;
|
|
78
|
-
entry.State = "running"; entry.Started = DateTimeOffset.UtcNow;
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public void Complete(Entry entry, Reply reply)
|
|
84
|
-
{
|
|
85
|
-
lock (_gate)
|
|
86
|
-
{
|
|
87
|
-
ExpireQueued(entry);
|
|
88
|
-
if (entry.Completion == null) return;
|
|
89
|
-
Finish(entry, reply.Outcome ?? (reply.Status < 400 ? "succeeded" : "failed"), reply);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
public object Status(string id)
|
|
94
|
-
{
|
|
95
|
-
var parts = id.Split(':');
|
|
96
|
-
if (parts.Length == 2 && Guid.TryParse(parts[1], out var uuid)) id = parts[0] + ":" + uuid.ToString("N");
|
|
97
|
-
lock (_gate)
|
|
98
|
-
{
|
|
99
|
-
if (!_entries.TryGetValue(id, out var entry)) return new
|
|
100
|
-
{
|
|
101
|
-
operation_id = id, bridge_id = Generation, state = "unknown",
|
|
102
|
-
message = "No receipt exists in this bridge session. The outcome is unknown; this does not prove that the operation never executed. Check the original bridge and model before retrying.",
|
|
103
|
-
};
|
|
104
|
-
ExpireQueued(entry);
|
|
105
|
-
return new
|
|
106
|
-
{
|
|
107
|
-
operation_id = id, bridge_id = Generation, tool = entry.Tool, state = entry.State,
|
|
108
|
-
created_at = entry.Created, started_at = entry.Started, finished_at = entry.Finished,
|
|
109
|
-
result_available = entry.SavedReply != null, result_http_status = entry.SavedReply?.Status,
|
|
110
|
-
result = entry.SavedReply?.Response,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private void ExpireQueued(Entry entry)
|
|
116
|
-
{
|
|
117
|
-
if (entry.State == "queued" && Stopwatch.GetTimestamp() > entry.Deadline)
|
|
118
|
-
Finish(entry, "expired_before_start", new Reply(408, new { error = true, operation_id = entry.Id,
|
|
119
|
-
message = "Operation expired before it started; no tool action was performed." }));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private void Finish(Entry entry, string state, Reply reply)
|
|
123
|
-
{
|
|
124
|
-
var completion = entry.Completion!;
|
|
125
|
-
// Keep a detached JSON value, never API objects or callbacks, in the receipt cache.
|
|
126
|
-
byte[] bytes;
|
|
127
|
-
try { bytes = JsonSerializer.SerializeToUtf8Bytes(reply.Response); }
|
|
128
|
-
catch (Exception error)
|
|
129
|
-
{
|
|
130
|
-
state = "result_unavailable";
|
|
131
|
-
reply = new Reply(500, new { error = true, message = "The tool finished but its receipt could not be serialized. Model or other effects may already have occurred. Inspect before retrying. " + error.Message });
|
|
132
|
-
bytes = JsonSerializer.SerializeToUtf8Bytes(reply.Response);
|
|
133
|
-
}
|
|
134
|
-
entry.State = state; entry.Finished = DateTimeOffset.UtcNow;
|
|
135
|
-
entry.SavedReply = new Reply(reply.Status, JsonSerializer.Deserialize<JsonElement>(bytes));
|
|
136
|
-
entry.SavedBytes = bytes.Length; _bytes += bytes.Length;
|
|
137
|
-
_results.Enqueue(entry);
|
|
138
|
-
entry.Completion = null;
|
|
139
|
-
while (_results.Count > _maxResults || _bytes > _maxBytes)
|
|
140
|
-
{
|
|
141
|
-
var old = _results.Dequeue();
|
|
142
|
-
_bytes -= old.SavedBytes; old.SavedBytes = 0; old.SavedReply = null;
|
|
143
|
-
}
|
|
144
|
-
completion.TrySetResult(reply);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private string ValidateId(string id)
|
|
148
|
-
{
|
|
149
|
-
string[] parts = id.Split(':');
|
|
150
|
-
if (parts.Length != 2 || parts[0] != Generation || !Guid.TryParse(parts[1], out _))
|
|
151
|
-
throw new ArgumentException("operation_id must contain this bridge session's bridgeId and a UUID. An ID from another bridge/restart cannot be replayed; its outcome must be checked separately.");
|
|
152
|
-
return parts[0] + ":" + Guid.Parse(parts[1]).ToString("N");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private static string Fingerprint(string tool, JsonElement args)
|
|
156
|
-
{
|
|
157
|
-
using var stream = new MemoryStream();
|
|
158
|
-
using (var writer = new Utf8JsonWriter(stream))
|
|
159
|
-
{
|
|
160
|
-
void Write(JsonElement element)
|
|
161
|
-
{
|
|
162
|
-
if (element.ValueKind == JsonValueKind.Object)
|
|
163
|
-
{
|
|
164
|
-
writer.WriteStartObject();
|
|
165
|
-
foreach (var property in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal)) { writer.WritePropertyName(property.Name); Write(property.Value); }
|
|
166
|
-
writer.WriteEndObject();
|
|
167
|
-
}
|
|
168
|
-
else if (element.ValueKind == JsonValueKind.Array)
|
|
169
|
-
{
|
|
170
|
-
writer.WriteStartArray(); foreach (var item in element.EnumerateArray()) Write(item); writer.WriteEndArray();
|
|
171
|
-
}
|
|
172
|
-
else element.WriteTo(writer);
|
|
173
|
-
}
|
|
174
|
-
writer.WriteStartArray(); writer.WriteStringValue(tool); Write(args); writer.WriteEndArray();
|
|
175
|
-
}
|
|
176
|
-
return Convert.ToHexString(SHA256.HashData(stream.ToArray()));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
1
|
+
using System.Diagnostics;
|
|
2
|
+
using System.Security.Cryptography;
|
|
3
|
+
using System.Text.Json;
|
|
4
|
+
|
|
5
|
+
namespace RevitBridge;
|
|
6
|
+
|
|
7
|
+
/// <summary>Session-local operation receipts. Expiring a result never permits the same ID to execute again.</summary>
|
|
8
|
+
internal sealed class OperationStore
|
|
9
|
+
{
|
|
10
|
+
internal sealed record Reply(int Status, object Response, string? Outcome = null);
|
|
11
|
+
internal sealed class Entry
|
|
12
|
+
{
|
|
13
|
+
internal required string Id;
|
|
14
|
+
internal required string Tool;
|
|
15
|
+
internal required string Fingerprint;
|
|
16
|
+
internal required long Deadline;
|
|
17
|
+
internal DateTimeOffset Created = DateTimeOffset.UtcNow;
|
|
18
|
+
internal DateTimeOffset? Started;
|
|
19
|
+
internal DateTimeOffset? Finished;
|
|
20
|
+
internal string State = "queued";
|
|
21
|
+
internal TaskCompletionSource<Reply>? Completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
22
|
+
internal Reply? SavedReply;
|
|
23
|
+
internal int SavedBytes;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private readonly object _gate = new();
|
|
27
|
+
private readonly Dictionary<string, Entry> _entries = new(StringComparer.Ordinal);
|
|
28
|
+
private readonly Queue<Entry> _results = new();
|
|
29
|
+
private readonly int _maxRecords;
|
|
30
|
+
private readonly int _maxResults;
|
|
31
|
+
private readonly int _maxBytes;
|
|
32
|
+
private int _bytes;
|
|
33
|
+
public string Generation { get; } = Guid.NewGuid().ToString("N");
|
|
34
|
+
|
|
35
|
+
internal OperationStore(int maxRecords = 10000, int maxResults = 128, int maxBytes = 32 * 1024 * 1024)
|
|
36
|
+
{
|
|
37
|
+
_maxRecords = maxRecords; _maxResults = maxResults; _maxBytes = maxBytes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public (Entry Entry, bool IsNew) Reserve(string id, string tool, JsonElement args, TimeSpan timeout)
|
|
41
|
+
{
|
|
42
|
+
id = ValidateId(id);
|
|
43
|
+
string fingerprint = Fingerprint(tool, args);
|
|
44
|
+
lock (_gate)
|
|
45
|
+
{
|
|
46
|
+
if (_entries.TryGetValue(id, out var existing))
|
|
47
|
+
{
|
|
48
|
+
if (existing.Fingerprint != fingerprint) throw new ArgumentException("operation_id was already used with different tool arguments. No new operation was started.");
|
|
49
|
+
return (existing, false);
|
|
50
|
+
}
|
|
51
|
+
if (_entries.Count >= _maxRecords) throw new InvalidOperationException("Operation receipt capacity reached. Restart the bridge before starting new tracked operations; existing receipts remain queryable.");
|
|
52
|
+
var entry = new Entry { Id = id, Tool = tool, Fingerprint = fingerprint,
|
|
53
|
+
Deadline = Stopwatch.GetTimestamp() + (long)(timeout.TotalSeconds * Stopwatch.Frequency) };
|
|
54
|
+
_entries.Add(id, entry);
|
|
55
|
+
return (entry, true);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public Task<Reply> Wait(Entry entry)
|
|
60
|
+
{
|
|
61
|
+
lock (_gate)
|
|
62
|
+
{
|
|
63
|
+
ExpireQueued(entry);
|
|
64
|
+
return entry.Completion?.Task ?? Task.FromResult(entry.SavedReply ?? new Reply(409, new
|
|
65
|
+
{
|
|
66
|
+
error = true, operation_id = entry.Id, state = entry.State,
|
|
67
|
+
message = "The operation already finished, but its full result has expired. It was not repeated. Inspect current model state before deciding on a new operation.",
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public bool TryStart(Entry entry)
|
|
73
|
+
{
|
|
74
|
+
lock (_gate)
|
|
75
|
+
{
|
|
76
|
+
ExpireQueued(entry);
|
|
77
|
+
if (entry.State != "queued") return false;
|
|
78
|
+
entry.State = "running"; entry.Started = DateTimeOffset.UtcNow;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public void Complete(Entry entry, Reply reply)
|
|
84
|
+
{
|
|
85
|
+
lock (_gate)
|
|
86
|
+
{
|
|
87
|
+
ExpireQueued(entry);
|
|
88
|
+
if (entry.Completion == null) return;
|
|
89
|
+
Finish(entry, reply.Outcome ?? (reply.Status < 400 ? "succeeded" : "failed"), reply);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public object Status(string id)
|
|
94
|
+
{
|
|
95
|
+
var parts = id.Split(':');
|
|
96
|
+
if (parts.Length == 2 && Guid.TryParse(parts[1], out var uuid)) id = parts[0] + ":" + uuid.ToString("N");
|
|
97
|
+
lock (_gate)
|
|
98
|
+
{
|
|
99
|
+
if (!_entries.TryGetValue(id, out var entry)) return new
|
|
100
|
+
{
|
|
101
|
+
operation_id = id, bridge_id = Generation, state = "unknown",
|
|
102
|
+
message = "No receipt exists in this bridge session. The outcome is unknown; this does not prove that the operation never executed. Check the original bridge and model before retrying.",
|
|
103
|
+
};
|
|
104
|
+
ExpireQueued(entry);
|
|
105
|
+
return new
|
|
106
|
+
{
|
|
107
|
+
operation_id = id, bridge_id = Generation, tool = entry.Tool, state = entry.State,
|
|
108
|
+
created_at = entry.Created, started_at = entry.Started, finished_at = entry.Finished,
|
|
109
|
+
result_available = entry.SavedReply != null, result_http_status = entry.SavedReply?.Status,
|
|
110
|
+
result = entry.SavedReply?.Response,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private void ExpireQueued(Entry entry)
|
|
116
|
+
{
|
|
117
|
+
if (entry.State == "queued" && Stopwatch.GetTimestamp() > entry.Deadline)
|
|
118
|
+
Finish(entry, "expired_before_start", new Reply(408, new { error = true, operation_id = entry.Id,
|
|
119
|
+
message = "Operation expired before it started; no tool action was performed." }));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private void Finish(Entry entry, string state, Reply reply)
|
|
123
|
+
{
|
|
124
|
+
var completion = entry.Completion!;
|
|
125
|
+
// Keep a detached JSON value, never API objects or callbacks, in the receipt cache.
|
|
126
|
+
byte[] bytes;
|
|
127
|
+
try { bytes = JsonSerializer.SerializeToUtf8Bytes(reply.Response); }
|
|
128
|
+
catch (Exception error)
|
|
129
|
+
{
|
|
130
|
+
state = "result_unavailable";
|
|
131
|
+
reply = new Reply(500, new { error = true, message = "The tool finished but its receipt could not be serialized. Model or other effects may already have occurred. Inspect before retrying. " + error.Message });
|
|
132
|
+
bytes = JsonSerializer.SerializeToUtf8Bytes(reply.Response);
|
|
133
|
+
}
|
|
134
|
+
entry.State = state; entry.Finished = DateTimeOffset.UtcNow;
|
|
135
|
+
entry.SavedReply = new Reply(reply.Status, JsonSerializer.Deserialize<JsonElement>(bytes));
|
|
136
|
+
entry.SavedBytes = bytes.Length; _bytes += bytes.Length;
|
|
137
|
+
_results.Enqueue(entry);
|
|
138
|
+
entry.Completion = null;
|
|
139
|
+
while (_results.Count > _maxResults || _bytes > _maxBytes)
|
|
140
|
+
{
|
|
141
|
+
var old = _results.Dequeue();
|
|
142
|
+
_bytes -= old.SavedBytes; old.SavedBytes = 0; old.SavedReply = null;
|
|
143
|
+
}
|
|
144
|
+
completion.TrySetResult(reply);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private string ValidateId(string id)
|
|
148
|
+
{
|
|
149
|
+
string[] parts = id.Split(':');
|
|
150
|
+
if (parts.Length != 2 || parts[0] != Generation || !Guid.TryParse(parts[1], out _))
|
|
151
|
+
throw new ArgumentException("operation_id must contain this bridge session's bridgeId and a UUID. An ID from another bridge/restart cannot be replayed; its outcome must be checked separately.");
|
|
152
|
+
return parts[0] + ":" + Guid.Parse(parts[1]).ToString("N");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private static string Fingerprint(string tool, JsonElement args)
|
|
156
|
+
{
|
|
157
|
+
using var stream = new MemoryStream();
|
|
158
|
+
using (var writer = new Utf8JsonWriter(stream))
|
|
159
|
+
{
|
|
160
|
+
void Write(JsonElement element)
|
|
161
|
+
{
|
|
162
|
+
if (element.ValueKind == JsonValueKind.Object)
|
|
163
|
+
{
|
|
164
|
+
writer.WriteStartObject();
|
|
165
|
+
foreach (var property in element.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal)) { writer.WritePropertyName(property.Name); Write(property.Value); }
|
|
166
|
+
writer.WriteEndObject();
|
|
167
|
+
}
|
|
168
|
+
else if (element.ValueKind == JsonValueKind.Array)
|
|
169
|
+
{
|
|
170
|
+
writer.WriteStartArray(); foreach (var item in element.EnumerateArray()) Write(item); writer.WriteEndArray();
|
|
171
|
+
}
|
|
172
|
+
else element.WriteTo(writer);
|
|
173
|
+
}
|
|
174
|
+
writer.WriteStartArray(); writer.WriteStringValue(tool); Write(args); writer.WriteEndArray();
|
|
175
|
+
}
|
|
176
|
+
return Convert.ToHexString(SHA256.HashData(stream.ToArray()));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
using System.Text.Json;
|
|
2
|
-
using System.Text.Json.Nodes;
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using System.Text.Json.Nodes;
|
|
3
3
|
using Autodesk.Revit.DB;
|
|
4
4
|
using Autodesk.Revit.UI;
|
|
5
5
|
using RevitBridge.Tools;
|
|
@@ -15,9 +15,9 @@ namespace RevitBridge
|
|
|
15
15
|
internal sealed record ToolContext(Document? Document, UIApplication? UIApplication);
|
|
16
16
|
|
|
17
17
|
/// <summary>
|
|
18
|
-
/// Optional tool return shape: complete structured payload plus a display summary.
|
|
19
|
-
/// The bridge/extension expose complete bounded data or explicit result retrieval to
|
|
20
|
-
/// the model; structured details remain available for rendering and diagnostics.
|
|
18
|
+
/// Optional tool return shape: complete structured payload plus a display summary.
|
|
19
|
+
/// The bridge/extension expose complete bounded data or explicit result retrieval to
|
|
20
|
+
/// the model; structured details remain available for rendering and diagnostics.
|
|
21
21
|
/// </summary>
|
|
22
22
|
internal sealed record ToolOutput(object? Payload, string? CompactText = null);
|
|
23
23
|
|
|
@@ -32,10 +32,10 @@ namespace RevitBridge
|
|
|
32
32
|
object ParametersSchema { get; }
|
|
33
33
|
|
|
34
34
|
/// <summary>True only for tools that mutate the model. Write tools own their transaction.</summary>
|
|
35
|
-
bool Write => false;
|
|
36
|
-
|
|
37
|
-
/// <summary>Potential effects, independent of whether this particular call changes anything.</summary>
|
|
38
|
-
IReadOnlyList<string> Effects => Write ? new[] { "model" } : Array.Empty<string>();
|
|
35
|
+
bool Write => false;
|
|
36
|
+
|
|
37
|
+
/// <summary>Potential effects, independent of whether this particular call changes anything.</summary>
|
|
38
|
+
IReadOnlyList<string> Effects => Write ? new[] { "model" } : Array.Empty<string>();
|
|
39
39
|
|
|
40
40
|
/// <summary>
|
|
41
41
|
/// False only for tools that never touch the Revit API. They skip the bridge's
|
|
@@ -47,12 +47,37 @@ namespace RevitBridge
|
|
|
47
47
|
/// <summary>Activation tier metadata: "core" (default) or "advanced".</summary>
|
|
48
48
|
string Tier => "core";
|
|
49
49
|
|
|
50
|
+
/// <summary>
|
|
51
|
+
/// Document kinds the tool works in: DocumentKind.Both or DocumentKind.ProjectOnly. Every
|
|
52
|
+
/// bridge tool declares it (the documentation gate requires the declaration); the
|
|
53
|
+
/// dispatcher refuses other kinds before the tool runs.
|
|
54
|
+
/// </summary>
|
|
55
|
+
IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
|
|
56
|
+
|
|
50
57
|
/// <summary>Optional one-line "Available tools" system prompt entry.</summary>
|
|
51
58
|
string? PromptSnippet => null;
|
|
52
59
|
|
|
53
60
|
/// <summary>Optional system prompt guideline bullets. Each bullet must name the tool.</summary>
|
|
54
61
|
IReadOnlyList<string>? PromptGuidelines => null;
|
|
55
62
|
|
|
63
|
+
/// <summary>
|
|
64
|
+
/// English discovery terms beyond the name and description: synonyms, outcome words
|
|
65
|
+
/// ("screenshot"), verbs and Revit nouns. Other UI languages are mapped onto these
|
|
66
|
+
/// terms by discovery alias files, never by translating tool metadata.
|
|
67
|
+
/// </summary>
|
|
68
|
+
IReadOnlyList<string> Keywords => Array.Empty<string>();
|
|
69
|
+
|
|
70
|
+
/// <summary>Declared capability boundaries, each with an alternative route.</summary>
|
|
71
|
+
IReadOnlyList<ToolLimit> Limits => Array.Empty<ToolLimit>();
|
|
72
|
+
|
|
73
|
+
/// <summary>
|
|
74
|
+
/// Minimum sufficient check that the requested outcome happened: "reread" (query the
|
|
75
|
+
/// changed state again), "capture" (inspect an image of the visible result),
|
|
76
|
+
/// "inspect_output" (open the produced file) or "none" (no durable outcome to check).
|
|
77
|
+
/// Required when the tool can write or has effects; null for pure reads.
|
|
78
|
+
/// </summary>
|
|
79
|
+
string? Verification => null;
|
|
80
|
+
|
|
56
81
|
/// <summary>
|
|
57
82
|
/// Runs on the Revit API thread (or the server task when RequiresDocument is false).
|
|
58
83
|
/// Returns a ToolOutput or any plain JSON-serializable payload.
|
|
@@ -79,25 +104,25 @@ namespace RevitBridge
|
|
|
79
104
|
registry.Add(new SetParameters());
|
|
80
105
|
registry.Add(new CaptureView());
|
|
81
106
|
registry.Add(new ExportDocuments());
|
|
82
|
-
registry.Add(new GetModelHealth());
|
|
83
|
-
registry.Add(new GetLinkedModels());
|
|
84
|
-
registry.Add(new GetLinkedElements());
|
|
85
|
-
registry.Add(new GetSchedules());
|
|
86
|
-
registry.Add(new GetElementRelationships());
|
|
87
|
-
registry.Add(new SummarizeElements());
|
|
88
|
-
registry.Add(new ManageElementSets());
|
|
89
|
-
registry.Add(new TransformElements());
|
|
90
|
-
registry.Add(new Tools.DeleteElements());
|
|
91
|
-
registry.Add(new ChangeElementTypes());
|
|
92
|
-
registry.Add(new ManageViews());
|
|
93
|
-
registry.Add(new ManageSheets());
|
|
94
|
-
registry.Add(new ManageSheetPlacements());
|
|
95
|
-
registry.Add(new GetScheduleFields());
|
|
96
|
-
registry.Add(new ManageSchedules());
|
|
97
|
-
registry.Add(new CreateTags());
|
|
98
|
-
registry.Add(new QuerySpatialElements());
|
|
99
|
-
registry.Add(new MeasureGeometry());
|
|
100
|
-
registry.Add(new GetModelCoordinates());
|
|
107
|
+
registry.Add(new GetModelHealth());
|
|
108
|
+
registry.Add(new GetLinkedModels());
|
|
109
|
+
registry.Add(new GetLinkedElements());
|
|
110
|
+
registry.Add(new GetSchedules());
|
|
111
|
+
registry.Add(new GetElementRelationships());
|
|
112
|
+
registry.Add(new SummarizeElements());
|
|
113
|
+
registry.Add(new ManageElementSets());
|
|
114
|
+
registry.Add(new TransformElements());
|
|
115
|
+
registry.Add(new Tools.DeleteElements());
|
|
116
|
+
registry.Add(new ChangeElementTypes());
|
|
117
|
+
registry.Add(new ManageViews());
|
|
118
|
+
registry.Add(new ManageSheets());
|
|
119
|
+
registry.Add(new ManageSheetPlacements());
|
|
120
|
+
registry.Add(new GetScheduleFields());
|
|
121
|
+
registry.Add(new ManageSchedules());
|
|
122
|
+
registry.Add(new CreateTags());
|
|
123
|
+
registry.Add(new QuerySpatialElements());
|
|
124
|
+
registry.Add(new MeasureGeometry());
|
|
125
|
+
registry.Add(new GetModelCoordinates());
|
|
101
126
|
return registry;
|
|
102
127
|
}
|
|
103
128
|
|
|
@@ -121,37 +146,43 @@ namespace RevitBridge
|
|
|
121
146
|
description = tool.Description,
|
|
122
147
|
category = tool.Write ? "write" : "read",
|
|
123
148
|
tier = tool.Tier,
|
|
124
|
-
parameters = DescribeParameters(tool),
|
|
149
|
+
parameters = DescribeParameters(tool),
|
|
125
150
|
executionMode = "sequential",
|
|
126
|
-
write = tool.Write,
|
|
127
|
-
effects = tool.Effects,
|
|
151
|
+
write = tool.Write,
|
|
152
|
+
effects = tool.Effects,
|
|
128
153
|
requiresDocument = tool.RequiresDocument,
|
|
154
|
+
documentKinds = tool.DocumentKinds,
|
|
155
|
+
keywords = tool.Keywords,
|
|
156
|
+
limits = tool.Limits.Select(limit => new { what = limit.What, alternative = new { kind = limit.Alternative, @ref = limit.Ref } }).ToArray(),
|
|
157
|
+
verification = tool.Verification,
|
|
129
158
|
promptSnippet = tool.PromptSnippet,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
properties["expected_document_id"] = new JsonObject
|
|
144
|
-
{
|
|
145
|
-
["type"] = "string",
|
|
146
|
-
["description"] =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
schema["required"]
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
// Cross-cutting rules (document identity, manuals, capability and completion
|
|
160
|
+
// protocols) are stated once by the Pi extension's platform section, not
|
|
161
|
+
// repeated per tool; guidelines here are tool-specific.
|
|
162
|
+
promptGuidelines = tool.PromptGuidelines,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
private static object DescribeParameters(ITool tool)
|
|
166
|
+
{
|
|
167
|
+
if (!tool.RequiresDocument) return tool.ParametersSchema;
|
|
168
|
+
var schema = JsonSerializer.SerializeToNode(tool.ParametersSchema)!.AsObject();
|
|
169
|
+
var properties = schema["properties"]!.AsObject();
|
|
170
|
+
bool identityRequired = tool.Write || DocumentGuard.AlwaysRequiresIdentity(tool.Name);
|
|
171
|
+
// The description must agree with requiredness in the same schema.
|
|
172
|
+
properties["expected_document_id"] = new JsonObject
|
|
173
|
+
{
|
|
174
|
+
["type"] = "string",
|
|
175
|
+
["description"] = identityRequired
|
|
176
|
+
? "Required: exact opaque project.documentId from get_model_overview for the intended open document. Invalid after close/reopen or bridge restart."
|
|
177
|
+
: "Exact opaque project.documentId from get_model_overview. Not required by the schema for reads, but required at runtime for any model change or UI change this tool performs; when supplied, the call runs only against that document. Invalid after close/reopen or bridge restart."
|
|
178
|
+
};
|
|
179
|
+
if (identityRequired)
|
|
180
|
+
{
|
|
181
|
+
var required = schema["required"] as JsonArray ?? new JsonArray();
|
|
182
|
+
if (!required.Any(x => x?.GetValue<string>() == "expected_document_id")) required.Add("expected_document_id");
|
|
183
|
+
schema["required"] = required;
|
|
184
|
+
}
|
|
185
|
+
return schema;
|
|
186
|
+
}
|
|
156
187
|
}
|
|
157
188
|
}
|
|
@@ -19,8 +19,16 @@ namespace RevitBridge.Tools
|
|
|
19
19
|
/// Long enough that a capture is never pulled out from under a session still reading it.</summary>
|
|
20
20
|
private static readonly TimeSpan CaptureRetention = TimeSpan.FromHours(24);
|
|
21
21
|
|
|
22
|
-
public string Name => "capture_view";
|
|
23
|
-
public IReadOnlyList<string>
|
|
22
|
+
public string Name => "capture_view";
|
|
23
|
+
public IReadOnlyList<string> DocumentKinds => DocumentKind.Both;
|
|
24
|
+
public IReadOnlyList<string> Keywords => new[] { "screenshot", "image", "png", "picture", "snapshot", "visual check" };
|
|
25
|
+
public IReadOnlyList<ToolLimit> Limits => new[]
|
|
26
|
+
{
|
|
27
|
+
new ToolLimit("Image contents or visual judgement (returns a file path only)", "tool", "read (open the returned PNG)"),
|
|
28
|
+
new ToolLimit("Deliverable PDF, DWG or IFC files", "tool", "export_documents"),
|
|
29
|
+
};
|
|
30
|
+
public string? Verification => "inspect_output";
|
|
31
|
+
public IReadOnlyList<string> Effects => new[] { "files" };
|
|
24
32
|
public string Label => "Capture View";
|
|
25
33
|
public string Description => "Export a PNG snapshot of a Revit view to a temporary file and return its path — the response contains NO image data; open the returned filePath with the read tool to actually see the image. Defaults to the active view; pass view_id for any other graphical view or sheet (find ids with get_elements, category 'Views' or 'Sheets'). The long image edge is capped at 1568 px. Schedules and view templates cannot be captured.";
|
|
26
34
|
public string Tier => "advanced";
|