@pascal-app/mcp 1.0.6 → 1.1.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 (88) hide show
  1. package/README.md +32 -11
  2. package/dist/bin/pascal-mcp.js +2 -0
  3. package/dist/bridge/agent-session.d.ts +2 -1
  4. package/dist/bridge/agent-session.d.ts.map +1 -1
  5. package/dist/bridge/agent-session.js +0 -1
  6. package/dist/bridge/scene-bridge.d.ts +22 -14
  7. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  8. package/dist/bridge/scene-bridge.js +180 -198
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/lib/safe-fetch.d.ts +6 -0
  12. package/dist/lib/safe-fetch.d.ts.map +1 -1
  13. package/dist/lib/safe-fetch.js +135 -41
  14. package/dist/operations/scene-operations.d.ts +7 -0
  15. package/dist/operations/scene-operations.d.ts.map +1 -1
  16. package/dist/operations/scene-operations.js +4 -0
  17. package/dist/prompts/from-brief.js +1 -1
  18. package/dist/prompts/iterate-on-feedback.js +1 -1
  19. package/dist/prompts/renovation-from-photos.js +1 -1
  20. package/dist/prompts/scene-guidance.js +2 -2
  21. package/dist/resources/agent-guide.d.ts.map +1 -1
  22. package/dist/resources/agent-guide.js +8 -3
  23. package/dist/resources/catalog-items.d.ts +4 -1
  24. package/dist/resources/catalog-items.d.ts.map +1 -1
  25. package/dist/resources/catalog-items.js +14 -6
  26. package/dist/server.d.ts +9 -5
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +21 -5
  29. package/dist/templates/empty-studio.d.ts.map +1 -1
  30. package/dist/templates/empty-studio.js +2 -3
  31. package/dist/templates/garden-house.d.ts.map +1 -1
  32. package/dist/templates/garden-house.js +2 -3
  33. package/dist/templates/two-bedroom.d.ts.map +1 -1
  34. package/dist/templates/two-bedroom.js +2 -3
  35. package/dist/tools/add-column.d.ts.map +1 -1
  36. package/dist/tools/add-column.js +9 -6
  37. package/dist/tools/add-object.d.ts +44 -5
  38. package/dist/tools/add-object.d.ts.map +1 -1
  39. package/dist/tools/add-object.js +75 -21
  40. package/dist/tools/apply-patch.js +1 -1
  41. package/dist/tools/asset-catalog.d.ts +5 -6
  42. package/dist/tools/asset-catalog.d.ts.map +1 -1
  43. package/dist/tools/asset-catalog.js +1 -9
  44. package/dist/tools/author-scene.d.ts +8 -5
  45. package/dist/tools/author-scene.d.ts.map +1 -1
  46. package/dist/tools/author-scene.js +302 -51
  47. package/dist/tools/clear-scene.js +2 -2
  48. package/dist/tools/collections.d.ts.map +1 -1
  49. package/dist/tools/collections.js +8 -5
  50. package/dist/tools/honest-patch-guard.js +6 -5
  51. package/dist/tools/hosted-services.d.ts.map +1 -1
  52. package/dist/tools/hosted-services.js +2 -2
  53. package/dist/tools/index.d.ts +7 -2
  54. package/dist/tools/index.d.ts.map +1 -1
  55. package/dist/tools/index.js +3 -9
  56. package/dist/tools/local-geometry-scripts.d.ts.map +1 -1
  57. package/dist/tools/local-geometry-scripts.js +7 -2
  58. package/dist/tools/patch-guards.d.ts.map +1 -1
  59. package/dist/tools/patch-guards.js +32 -15
  60. package/dist/tools/project-files.d.ts.map +1 -1
  61. package/dist/tools/project-files.js +2 -2
  62. package/dist/tools/room-tools.d.ts +1 -0
  63. package/dist/tools/room-tools.d.ts.map +1 -1
  64. package/dist/tools/room-tools.js +11 -8
  65. package/dist/tools/scene-scope.d.ts +4 -0
  66. package/dist/tools/scene-scope.d.ts.map +1 -0
  67. package/dist/tools/scene-scope.js +4 -0
  68. package/dist/tools/shared-tools.d.ts +0 -2
  69. package/dist/tools/shared-tools.d.ts.map +1 -1
  70. package/dist/tools/shared-tools.js +146 -31
  71. package/dist/tools/templates/create-house-from-brief.js +1 -1
  72. package/dist/tools/view-scene.js +2 -2
  73. package/examples/coordinate-conventions-demo.md +1 -0
  74. package/examples/photo-to-scene.md +1 -0
  75. package/examples/renovate-from-photos.md +2 -0
  76. package/package.json +10 -10
  77. package/dist/tools/create-unit.d.ts +0 -29
  78. package/dist/tools/create-unit.d.ts.map +0 -1
  79. package/dist/tools/create-unit.js +0 -36
  80. package/dist/tools/list-units.d.ts +0 -38
  81. package/dist/tools/list-units.d.ts.map +0 -1
  82. package/dist/tools/list-units.js +0 -61
  83. package/dist/tools/set-unit-members.d.ts +0 -22
  84. package/dist/tools/set-unit-members.d.ts.map +0 -1
  85. package/dist/tools/set-unit-members.js +0 -43
  86. package/dist/tools/unit-members.d.ts +0 -4
  87. package/dist/tools/unit-members.d.ts.map +0 -1
  88. package/dist/tools/unit-members.js +0 -24
@@ -1 +1 @@
1
- {"version":3,"file":"patch-guards.d.ts","sourceRoot":"","sources":["../../src/tools/patch-guards.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAKvB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,OAAO,EAAE,KAAK,SAAS,EAAyB,MAAM,yBAAyB,CAAA;AACxF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAEnD,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,iBAAiB,GACjB,iBAAiB,GACjB,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,gBAAgB,GAEhB,eAAe,GACf,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,GAEhB,YAAY,GACZ,yBAAyB,GACzB,mBAAmB,CAAA;AAEvB;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CAAA;CACjF,CAAA;AAID,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,QAKnD;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,KAAK,EAAE,EAChB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,MAAM,EAAE,CAKV;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,IAAI,EAAE,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM;IAHzB,YACW,IAAI,EAAE,gBAAgB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,EAIf;CACF;AAkDD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,SAAS,KAAK,EAAE,EACzB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,WAAW,EAAE,SAAS,SAAS,EAAE,EACjC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,gBAAgB,GAC9E,IAAI,CA+KN"}
1
+ {"version":3,"file":"patch-guards.d.ts","sourceRoot":"","sources":["../../src/tools/patch-guards.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAMvB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,OAAO,EAAE,KAAK,SAAS,EAAyB,MAAM,yBAAyB,CAAA;AACxF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAEnD,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,iBAAiB,GACjB,iBAAiB,GACjB,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,gBAAgB,GAEhB,eAAe,GACf,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,GAEhB,YAAY,GACZ,yBAAyB,GACzB,mBAAmB,CAAA;AAEvB;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CAAA;CACjF,CAAA;AAID,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,QAKnD;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,KAAK,EAAE,EAChB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,MAAM,EAAE,CAKV;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,IAAI,EAAE,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM;IAHzB,YACW,IAAI,EAAE,gBAAgB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM,EAIf;CACF;AAkDD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,SAAS,KAAK,EAAE,EACzB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,WAAW,EAAE,SAAS,SAAS,EAAE,EACjC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,gBAAgB,GAC9E,IAAI,CAwMN"}
@@ -1,4 +1,4 @@
1
- import { nodeRegistry, planNodeDeletion, previewDefaultGutterRefresh, validateNodeRelations, } from '@pascal-app/core';
1
+ import { nodeRegistry, planNodeDeletion, previewDefaultGutterRefresh, validateNodeRelations, wouldCreateHierarchyCycle, } from '@pascal-app/core';
2
2
  import { scriptedFieldRefusal, unknownMaterialPresetRefusal, } from '@pascal-app/core/agent-operations';
3
3
  import { AnyNode, nodeKindOf, parseNode } from '@pascal-app/core/schema';
4
4
  const guards = [];
@@ -104,15 +104,25 @@ function withoutChild(parent, childId) {
104
104
  * Lives in the tool layer so every bridge behind `apply_patch` inherits it.
105
105
  */
106
106
  export function assertPatchKeepsIdentity(patches, nodes, rootNodeIds, planDeletion) {
107
- const initialIds = new Set(Object.keys(nodes));
108
- let scene = {
109
- nodes: { ...nodes },
110
- rootNodeIds: [...rootNodeIds],
111
- collections: {},
107
+ // What the patch writes, over the scene; the whole scene is copied only for a preview that reads
108
+ // all of it (deletes, gutter refreshes, a reparent), so an edit reads only the ids it names.
109
+ const written = new Map();
110
+ let whole = null;
111
+ const scene = () => {
112
+ if (!whole) {
113
+ const all = { ...nodes };
114
+ for (const [id, node] of written)
115
+ all[id] = node;
116
+ whole = { nodes: all, rootNodeIds: [...rootNodeIds], collections: {} };
117
+ }
118
+ return whole;
112
119
  };
113
- const at = (id) => scene.nodes[id];
120
+ const at = (id) => whole ? whole.nodes[id] : (written.get(id) ?? nodes[id]);
114
121
  const put = (node) => {
115
- scene.nodes[node.id] = node;
122
+ if (whole)
123
+ whole.nodes[node.id] = node;
124
+ else
125
+ written.set(node.id, node);
116
126
  };
117
127
  // Default gutters and downspouts a delete run refreshes: the store may keep,
118
128
  // move or replace them with freshly minted ids, so later ops in the same
@@ -133,7 +143,7 @@ export function assertPatchKeepsIdentity(patches, nodes, rootNodeIds, planDeleti
133
143
  const roofId = node.type === 'roof' ? node.id : node.type === 'roof-segment' ? node.parentId : null;
134
144
  if (!roofId)
135
145
  return;
136
- const preview = previewDefaultGutterRefresh(scene.nodes, [roofId]);
146
+ const preview = previewDefaultGutterRefresh(scene().nodes, [roofId]);
137
147
  for (const id of preview.unsettledIds)
138
148
  regenerated.set(id, index);
139
149
  for (const id of preview.regeneratedHostIds)
@@ -142,13 +152,13 @@ export function assertPatchKeepsIdentity(patches, nodes, rootNodeIds, planDeleti
142
152
  let pendingDeletes = [];
143
153
  const flushDeletes = (index) => {
144
154
  const plan = planDeletion
145
- ? planDeletion(scene, pendingDeletes)
146
- : planNodeDeletion(scene, pendingDeletes, { mintDefaults: false });
155
+ ? planDeletion(scene(), pendingDeletes)
156
+ : planNodeDeletion(scene(), pendingDeletes, { mintDefaults: false });
147
157
  for (const id of plan.unsettledIds)
148
158
  regenerated.set(id, index);
149
159
  for (const id of plan.regeneratedHostIds)
150
160
  regeneratedHosts.set(id, index);
151
- scene = { nodes: plan.nodes, rootNodeIds: plan.rootNodeIds, collections: plan.collections };
161
+ whole = { nodes: plan.nodes, rootNodeIds: plan.rootNodeIds, collections: plan.collections };
152
162
  pendingDeletes = [];
153
163
  };
154
164
  patches.forEach((patch, index) => {
@@ -166,7 +176,7 @@ export function assertPatchKeepsIdentity(patches, nodes, rootNodeIds, planDeleti
166
176
  const effectiveParentId = patch.parentId ?? node.parentId;
167
177
  if (effectiveParentId)
168
178
  refuseRegenerated(index, effectiveParentId);
169
- if (at(node.id) || (!planDeletion && initialIds.has(node.id))) {
179
+ if (at(node.id) || (!planDeletion && nodes[node.id] !== undefined)) {
170
180
  throw new PatchRefusedError('node_exists', index, node.id, planDeletion || at(node.id)
171
181
  ? `create id "${node.id}" already exists. Use op "update" to change it, or delete it earlier in the same patch to replace it.`
172
182
  : `create id "${node.id}" existed when the patch started, and this bridge cannot preview its deletes. Delete it in one apply_patch call and create it in the next.`);
@@ -175,6 +185,9 @@ export function assertPatchKeepsIdentity(patches, nodes, rootNodeIds, planDeleti
175
185
  if (patch.parentId !== undefined && !at(patch.parentId)) {
176
186
  throw new Error(`invalid patch: patches[${index}] create parentId "${patch.parentId}" not found`);
177
187
  }
188
+ if (wouldCreateHierarchyCycle(node.id, parentId, at)) {
189
+ throw new PatchRefusedError('invalid_parent', index, node.id, `create cannot place "${node.id}" under ${JSON.stringify(parentId)}: it would create a hierarchy cycle.`);
190
+ }
178
191
  put({ ...node, parentId });
179
192
  const parent = parentId ? at(parentId) : undefined;
180
193
  if (parent)
@@ -217,11 +230,15 @@ export function assertPatchKeepsIdentity(patches, nodes, rootNodeIds, planDeleti
217
230
  }
218
231
  }
219
232
  if ('parentId' in data && data.parentId !== current.parentId) {
233
+ const newParentId = typeof data.parentId === 'string' ? data.parentId : null;
234
+ if (wouldCreateHierarchyCycle(patch.id, newParentId, at)) {
235
+ throw new PatchRefusedError('invalid_parent', index, patch.id, `update cannot move "${patch.id}" under ${JSON.stringify(data.parentId)}: it would create a hierarchy cycle.`);
236
+ }
220
237
  const newParent = typeof data.parentId === 'string' ? at(data.parentId) : undefined;
221
238
  if (!(newParent && Array.isArray(newParent.children))) {
222
239
  throw new PatchRefusedError('invalid_parent', index, patch.id, `update cannot move "${patch.id}" under ${JSON.stringify(data.parentId)}: ${newParent ? `a ${newParent.type} holds no children` : 'no such node'}.`);
223
240
  }
224
- const before = { ...scene.nodes };
241
+ const before = { ...scene().nodes };
225
242
  const oldParent = current.parentId ? at(current.parentId) : undefined;
226
243
  if (oldParent)
227
244
  put(withoutChild(oldParent, patch.id));
@@ -229,7 +246,7 @@ export function assertPatchKeepsIdentity(patches, nodes, rootNodeIds, planDeleti
229
246
  put(merged);
230
247
  unsettleRoofRefresh(merged, index);
231
248
  try {
232
- validateNodeRelations(before, scene.nodes, [patch.id]);
249
+ validateNodeRelations(before, scene().nodes, [patch.id]);
233
250
  }
234
251
  catch (err) {
235
252
  throw new PatchRefusedError('invalid_parent', index, patch.id, `${newParent.type} "${newParent.id}" does not accept "${patch.id}": ${err instanceof Error ? err.message : String(err)}`);
@@ -1 +1 @@
1
- {"version":3,"file":"project-files.d.ts","sourceRoot":"","sources":["../../src/tools/project-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mCAAmC,CAAA;AAQ1C,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,uBAAuB,QAmC3F"}
1
+ {"version":3,"file":"project-files.d.ts","sourceRoot":"","sources":["../../src/tools/project-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mCAAmC,CAAA;AAS1C,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,uBAAuB,QAmC3F"}
@@ -1,11 +1,11 @@
1
1
  import { executeProjectFileTool, } from '@pascal-app/core/agent-operations';
2
- import { isAgentRefusal, PROJECT_FILE_TOOL_CONTRACTS, refusalPayload, } from '@pascal-app/core/agent-tools';
2
+ import { isAgentRefusal, PROJECT_FILE_TOOL_CONTRACTS, refusalPayload, surfaceDescription, } from '@pascal-app/core/agent-tools';
3
3
  import { z } from 'zod';
4
4
  export function registerProjectFileTools(server, execute) {
5
5
  for (const contract of PROJECT_FILE_TOOL_CONTRACTS) {
6
6
  server.registerTool(contract.name, {
7
7
  title: contract.title,
8
- description: contract.description,
8
+ description: surfaceDescription(contract, 'mcp'),
9
9
  inputSchema: z.object(contract.input).strict(),
10
10
  annotations: contract.annotations,
11
11
  }, async (input, ctx) => {
@@ -19,6 +19,7 @@ export declare const addDoorOutput: {
19
19
  unchanged: z.ZodOptional<z.ZodLiteral<true>>;
20
20
  }, z.core.$strip>;
21
21
  doorId: z.ZodString;
22
+ sillHeight: z.ZodOptional<z.ZodNumber>;
22
23
  project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
23
24
  unsaved: z.ZodOptional<z.ZodString>;
24
25
  persistence: z.ZodOptional<z.ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"room-tools.d.ts","sourceRoot":"","sources":["../../src/tools/room-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAyB7D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAmB,KAAK,kBAAkB,EAAc,MAAM,cAAc,CAAA;AAMnF,mFAAmF;AACnF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;CAA0C,CAAA;AACpE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;CAA4C,CAAA;AAwHxE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,eAAe,CAAC,EAAE,kBAAkB,GACnC,IAAI,CA8BN;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,eAAe,CAAC,EAAE,kBAAkB,GACnC,IAAI,CA8BN;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,eAAe,CAAC,EAAE,kBAAkB,GACnC,IAAI,CAGN"}
1
+ {"version":3,"file":"room-tools.d.ts","sourceRoot":"","sources":["../../src/tools/room-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AA0B7D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAmB,KAAK,kBAAkB,EAAc,MAAM,cAAc,CAAA;AAOnF,mFAAmF;AACnF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;CAA0C,CAAA;AACpE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;CAA4C,CAAA;AA0HxE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,eAAe,CAAC,EAAE,kBAAkB,GACnC,IAAI,CAiCN;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,eAAe,CAAC,EAAE,kBAAkB,GACnC,IAAI,CAiCN;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,eAAe,CAAC,EAAE,kBAAkB,GACnC,IAAI,CAGN"}
@@ -1,12 +1,13 @@
1
1
  import { isScriptedNode, scriptedObjectMeta } from '@pascal-app/core';
2
2
  import { achievedChanges, addWallOpening, editedScriptLanguage, editedScriptParams, rebuiltOpeningResult, rescriptOpening, } from '@pascal-app/core/agent-operations';
3
- import { addDoorOutput as addDoorResult, addDoorTool, addWindowOutput as addWindowResult, addWindowTool, isAgentRefusal, } from '@pascal-app/core/agent-tools';
3
+ import { addDoorOutput as addDoorResult, addDoorTool, addWindowOutput as addWindowResult, addWindowTool, isAgentRefusal, surfaceDescription, } from '@pascal-app/core/agent-tools';
4
4
  import { GeometryReuseFields } from '@pascal-app/core/schema';
5
5
  import { z } from 'zod';
6
6
  import { compileAndStore, readScript } from './add-object.js';
7
7
  import { ADDITIVE_TOOL_ANNOTATIONS } from './annotations.js';
8
8
  import { refusalResult, toolError } from './errors.js';
9
9
  import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync.js';
10
+ import { inScene } from './scene-scope.js';
10
11
  import { toPatches } from './shared-tools.js';
11
12
  /** The contract's one result (core `addWallOpening`), and the live sync's note. */
12
13
  export const addDoorOutput = { ...addDoorResult, ...liveSyncOutput };
@@ -43,7 +44,7 @@ async function rebuildOpening(kind, bridge, host, input) {
43
44
  ...GeometryReuseFields.parse(input),
44
45
  },
45
46
  });
46
- outcome = rescriptOpening(nodes, { ...input, compiled }, { activeLevelId: bridge.activeLevelId?.() ?? null });
47
+ outcome = inScene(bridge, () => rescriptOpening(nodes, { ...input, compiled }, { activeLevelId: bridge.activeLevelId?.() ?? null }));
47
48
  }
48
49
  catch (error) {
49
50
  if (isAgentRefusal(error))
@@ -94,7 +95,7 @@ async function compileOpeningScript(kind, bridge, host, input) {
94
95
  export function registerAddDoor(server, bridge, geometryScripts) {
95
96
  server.registerTool(addDoorTool.name, {
96
97
  title: addDoorTool.title,
97
- description: addDoorTool.description,
98
+ description: surfaceDescription(addDoorTool, 'mcp'),
98
99
  inputSchema: z.object(addDoorTool.input),
99
100
  outputSchema: z.object(addDoorOutput),
100
101
  annotations: ADDITIVE_TOOL_ANNOTATIONS,
@@ -106,11 +107,12 @@ export function registerAddDoor(server, bridge, geometryScripts) {
106
107
  return compiled.error;
107
108
  let opening;
108
109
  try {
109
- opening = addWallOpening(bridge.getNodes(), {
110
+ // Planned after the compile was awaited: back in the scene's scope for its ids.
111
+ opening = inScene(bridge, () => addWallOpening(bridge.getNodes(), {
110
112
  kind: 'door',
111
113
  ...input,
112
114
  compiled: compiled.script,
113
- });
115
+ }));
114
116
  }
115
117
  catch (error) {
116
118
  return refusalResult(error);
@@ -123,7 +125,7 @@ export function registerAddDoor(server, bridge, geometryScripts) {
123
125
  export function registerAddWindow(server, bridge, geometryScripts) {
124
126
  server.registerTool(addWindowTool.name, {
125
127
  title: addWindowTool.title,
126
- description: addWindowTool.description,
128
+ description: surfaceDescription(addWindowTool, 'mcp'),
127
129
  inputSchema: z.object(addWindowTool.input),
128
130
  outputSchema: z.object(addWindowOutput),
129
131
  annotations: ADDITIVE_TOOL_ANNOTATIONS,
@@ -135,11 +137,12 @@ export function registerAddWindow(server, bridge, geometryScripts) {
135
137
  return compiled.error;
136
138
  let opening;
137
139
  try {
138
- opening = addWallOpening(bridge.getNodes(), {
140
+ // Planned after the compile was awaited: back in the scene's scope for its ids.
141
+ opening = inScene(bridge, () => addWallOpening(bridge.getNodes(), {
139
142
  kind: 'window',
140
143
  ...input,
141
144
  compiled: compiled.script,
142
- });
145
+ }));
143
146
  }
144
147
  catch (error) {
145
148
  return refusalResult(error);
@@ -0,0 +1,4 @@
1
+ import type { SceneOperations } from '../operations/index.js';
2
+ /** Runs `run` in the scene's scope when the operations have one (`SceneOperations.runInScene`). */
3
+ export declare function inScene<T>(operations: SceneOperations, run: () => T): T;
4
+ //# sourceMappingURL=scene-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-scope.d.ts","sourceRoot":"","sources":["../../src/tools/scene-scope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,mGAAmG;AACnG,wBAAgB,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAEvE"}
@@ -0,0 +1,4 @@
1
+ /** Runs `run` in the scene's scope when the operations have one (`SceneOperations.runInScene`). */
2
+ export function inScene(operations, run) {
3
+ return operations.runInScene ? operations.runInScene(run) : run();
4
+ }
@@ -18,8 +18,6 @@ export type OperationTool = {
18
18
  annotations: AgentToolAnnotations;
19
19
  outputSchema?: Record<string, z.ZodType>;
20
20
  envelope?: (bridge: SceneOperations) => Record<string, unknown>;
21
- /** Reads the host's item library: only these calls wait for it (the hosted one is a query). */
22
- catalog?: true;
23
21
  };
24
22
  export type PlanReadingHosts = {
25
23
  /** Reads catalog pictures and surfaces before view_assets and paint. */
@@ -1 +1 @@
1
- {"version":3,"file":"shared-tools.d.ts","sourceRoot":"","sources":["../../src/tools/shared-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EACL,KAAK,cAAc,EAGnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,eAAe,EAUpB,KAAK,YAAY,EAGlB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAStB,KAAK,WAAW,EA+BjB,MAAM,8BAA8B,CAAA;AAErC,OAAO,EAGL,KAAK,sBAAsB,EAG5B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,iBAAiB,CAAA;AASnE;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,SAAS,EAAE,cAAc,CAAA;IACzB,WAAW,EAAE,oBAAoB,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/D,+FAA+F;IAC/F,OAAO,CAAC,EAAE,IAAI,CAAA;CACf,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wEAAwE;IACxE,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,wFAAwF;IACxF,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAA;CAC3F,CAAA;AAmaD,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,EAAE,CAmBxD;AAED,qGAAqG;AACrG,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,CAsBtE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,GAAG,IAAI,GAAG,SAAS,CAIpF;AAUD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,YAA6B,EACtC,KAAK,GAAE,gBAAqB,GAC3B,IAAI,CAGN;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,aAAa,EACnB,OAAO,GAAE,YAA6B,EACtC,KAAK,GAAE,gBAAqB,GAC3B,IAAI,CAuHN"}
1
+ {"version":3,"file":"shared-tools.d.ts","sourceRoot":"","sources":["../../src/tools/shared-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EACL,KAAK,cAAc,EAGnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,eAAe,EASpB,KAAK,YAAY,EAGlB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAUtB,KAAK,WAAW,EAyCjB,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EAGL,KAAK,sBAAsB,EAI5B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,iBAAiB,CAAA;AAUnE;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,SAAS,EAAE,cAAc,CAAA;IACzB,WAAW,EAAE,oBAAoB,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChE,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wEAAwE;IACxE,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,wFAAwF;IACxF,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAA;CAC3F,CAAA;AAoeD,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,EAAE,CAmBxD;AAED,qGAAqG;AACrG,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,CAyBtE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,GAAG,IAAI,GAAG,SAAS,CAIpF;AA+CD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE,YAA6B,EACtC,KAAK,GAAE,gBAAqB,GAC3B,IAAI,CAGN;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,aAAa,EACnB,OAAO,GAAE,YAA6B,EACtC,KAAK,GAAE,gBAAqB,GAC3B,IAAI,CAwIN"}
@@ -1,13 +1,15 @@
1
- import { AGENT_OPERATIONS, achievedChanges, applyAgentOutcome, assetViewNeeds, assetViewsReadCatalog, briefReadsCatalog, buildBatchObjects, createAssetViewLoader, createFetchAssetViewIO, furnishingRefusal, startOfSession, writeCollections, } from '@pascal-app/core/agent-operations';
2
- import { addCornerWindowTool, addFenceTool, addLevelTool, addObjectTool, addWallTool, createRoofTool, createRoomTool, createStairTool, deleteNodeTool, designBriefTool, duplicateLevelTool, findByTypeTool, fitStairTool, furnishRoomTool, getLevelSummaryTool, getNodeTool, getSiteTool, getWallsTool, getZonesTool, listLevelsTool, measureStairTool, mergeWindowsTool, moveRoofsToRoofLevelTool, paintTool, placeBuildingTool, placeDesignTool, placeInRoomTool, placeItemsTool, ROOM_TOOL_CONTRACTS, refuse, runBatchTool, searchAssetsTool, searchMaterialsTool, setCheckpointTool, setGroundLevelTool, validateDesignTool, verifySceneTool, viewAssetsTool, } from '@pascal-app/core/agent-tools';
1
+ import { AGENT_OPERATIONS, achievedChanges, applyAgentOutcome, assetViewNeeds, buildBatchObjects, createAssetViewLoader, createFetchAssetViewIO, furnishingRefusal, readCatalog, startOfSession, writeCollections, } from '@pascal-app/core/agent-operations';
2
+ import { addCornerWindowTool, addFenceTool, addLevelTool, addObjectTool, addWallTool, createRoofTool, createRoomTool, createStairTool, createUnitTool, deleteNodeTool, designBriefTool, duplicateLevelTool, duplicateNodeOutput, duplicateNodeTool, findByTypeTool, fitStairTool, furnishRoomTool, getLevelSummaryTool, getNodeTool, getSiteTool, getWallsTool, getZonesTool, isAgentRefusal, listLevelsTool, listUnitsTool, measureStairTool, mergeWindowsTool, moveNodeOutput, moveNodeTool, moveRoofsToRoofLevelTool, paintTool, placeBuildingTool, placeDesignTool, placeInRoomTool, placeItemsTool, ROOM_TOOL_CONTRACTS, refuse, runBatchTool, searchAssetsTool, searchMaterialsTool, setCheckpointTool, setGroundLevelTool, setUnitMembersTool, setWallTypeTool, surfaceDescription, validateDesignTool, verifySceneModelResult, verifySceneTool, viewAssetsTool, } from '@pascal-app/core/agent-tools';
3
3
  import { DesignValidationSchema } from '@pascal-app/core/procedural-items';
4
- import { MaterialSchema, SceneMaterial, } from '@pascal-app/core/schema';
4
+ import { untracked } from '@pascal-app/core/runtime';
5
+ import { MaterialSchema, SceneMaterial, UnitNode, } from '@pascal-app/core/schema';
5
6
  import { z } from 'zod';
6
7
  import { ADDITIVE_TOOL_ANNOTATIONS, DESTRUCTIVE_TOOL_ANNOTATIONS, READ_ONLY_TOOL_ANNOTATIONS, } from './annotations.js';
7
8
  import { builtInCatalog } from './asset-catalog.js';
8
9
  import { registerCollectionTools } from './collections.js';
9
10
  import { refusalResult } from './errors.js';
10
11
  import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync.js';
12
+ import { inScene } from './scene-scope.js';
11
13
  import { ROOM_TOOL_ANNOTATIONS, structureOutput } from './structure-tools.js';
12
14
  const defaultAssetViews = createAssetViewLoader(createFetchAssetViewIO());
13
15
  const jsonObject = z.record(z.string(), z.unknown());
@@ -118,7 +120,11 @@ const SHARED_TOOLS = [
118
120
  contract: getWallsTool,
119
121
  operation: AGENT_OPERATIONS.get_walls,
120
122
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
121
- outputSchema: { levelId: z.string(), walls: z.array(jsonObject) },
123
+ outputSchema: {
124
+ levelId: z.string(),
125
+ walls: z.array(jsonObject),
126
+ wallTypes: z.array(jsonObject),
127
+ },
122
128
  },
123
129
  {
124
130
  contract: getZonesTool,
@@ -181,7 +187,7 @@ const SHARED_TOOLS = [
181
187
  issues: z.array(z.looseObject({
182
188
  type: z.string(),
183
189
  message: z.string(),
184
- severity: z.literal('info').optional(),
190
+ severity: z.enum(['info', 'warning']).optional(),
185
191
  nodeId: z.string().optional(),
186
192
  path: z.string().optional(),
187
193
  zoneId: z.string().optional(),
@@ -230,6 +236,11 @@ const SHARED_TOOLS = [
230
236
  operation: AGENT_OPERATIONS.add_wall,
231
237
  annotations: ADDITIVE_TOOL_ANNOTATIONS,
232
238
  },
239
+ {
240
+ contract: setWallTypeTool,
241
+ operation: AGENT_OPERATIONS.set_wall_type,
242
+ annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
243
+ },
233
244
  {
234
245
  contract: addFenceTool,
235
246
  operation: AGENT_OPERATIONS.add_fence,
@@ -269,7 +280,6 @@ const SHARED_TOOLS = [
269
280
  contract: placeItemsTool,
270
281
  operation: AGENT_OPERATIONS.place_items,
271
282
  annotations: ADDITIVE_TOOL_ANNOTATIONS,
272
- catalog: true,
273
283
  },
274
284
  {
275
285
  contract: deleteNodeTool,
@@ -277,6 +287,18 @@ const SHARED_TOOLS = [
277
287
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
278
288
  outputSchema: { deletedIds: z.array(z.string()), ...achievedOutput, ...liveSyncOutput },
279
289
  },
290
+ {
291
+ contract: moveNodeTool,
292
+ operation: AGENT_OPERATIONS.move_node,
293
+ annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
294
+ outputSchema: { ...moveNodeOutput, ...liveSyncOutput },
295
+ },
296
+ {
297
+ contract: duplicateNodeTool,
298
+ operation: AGENT_OPERATIONS.duplicate_node,
299
+ annotations: ADDITIVE_TOOL_ANNOTATIONS,
300
+ outputSchema: { ...duplicateNodeOutput, ...liveSyncOutput },
301
+ },
280
302
  {
281
303
  contract: createRoomTool,
282
304
  operation: AGENT_OPERATIONS.create_room,
@@ -310,7 +332,6 @@ const SHARED_TOOLS = [
310
332
  contract: furnishRoomTool,
311
333
  operation: AGENT_OPERATIONS.furnish_room,
312
334
  annotations: ADDITIVE_TOOL_ANNOTATIONS,
313
- catalog: true,
314
335
  outputSchema: {
315
336
  ok: z.literal(true),
316
337
  placed: z.number(),
@@ -343,7 +364,6 @@ const SHARED_TOOLS = [
343
364
  contract: placeInRoomTool,
344
365
  operation: AGENT_OPERATIONS.place_in_room,
345
366
  annotations: ADDITIVE_TOOL_ANNOTATIONS,
346
- catalog: true,
347
367
  outputSchema: {
348
368
  ok: z.literal(true),
349
369
  itemId: z.string(),
@@ -379,13 +399,11 @@ const SHARED_TOOLS = [
379
399
  contract: searchAssetsTool,
380
400
  operation: AGENT_OPERATIONS.search_assets,
381
401
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
382
- catalog: true,
383
402
  },
384
403
  {
385
404
  contract: viewAssetsTool,
386
405
  operation: AGENT_OPERATIONS.view_assets,
387
406
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
388
- catalog: true,
389
407
  },
390
408
  {
391
409
  contract: searchMaterialsTool,
@@ -401,6 +419,51 @@ const SHARED_TOOLS = [
401
419
  outputSchema: { ...structureOutput, ...achievedOutput },
402
420
  };
403
421
  }),
422
+ {
423
+ contract: createUnitTool,
424
+ operation: AGENT_OPERATIONS.create_unit,
425
+ annotations: ADDITIVE_TOOL_ANNOTATIONS,
426
+ outputSchema: { unitId: z.string(), ...achievedOutput, ...liveSyncOutput },
427
+ },
428
+ {
429
+ contract: setUnitMembersTool,
430
+ operation: AGENT_OPERATIONS.set_unit_members,
431
+ annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
432
+ outputSchema: {
433
+ unitId: z.string(),
434
+ memberCount: z.number(),
435
+ ...achievedOutput,
436
+ ...liveSyncOutput,
437
+ },
438
+ },
439
+ {
440
+ contract: listUnitsTool,
441
+ operation: AGENT_OPERATIONS.list_units,
442
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
443
+ outputSchema: {
444
+ units: z.array(z.object({
445
+ unitId: z.string(),
446
+ name: z.string(),
447
+ kind: UnitNode.shape.kind,
448
+ color: z.string(),
449
+ members: z.array(z.string()),
450
+ report: z.object({
451
+ memberCount: z.number(),
452
+ levelSpan: z
453
+ .object({ minOrdinal: z.number(), maxOrdinal: z.number(), count: z.number() })
454
+ .nullable(),
455
+ grossAreaM2: z.number(),
456
+ members: z.array(z.object({
457
+ zoneId: z.string(),
458
+ name: z.string(),
459
+ levelId: z.string().nullable(),
460
+ levelOrdinal: z.number().nullable(),
461
+ areaM2: z.number(),
462
+ })),
463
+ }),
464
+ })),
465
+ },
466
+ },
404
467
  {
405
468
  contract: runBatchTool,
406
469
  operation: AGENT_OPERATIONS.run_batch,
@@ -439,7 +502,8 @@ export function agentRuntime(bridge) {
439
502
  if (next.length)
440
503
  bridge.applyPatch(next);
441
504
  if (changes.collections && Object.keys(changes.collections).length)
442
- bridge.setCollections(writeCollections(bridge.getCollections(), changes.collections));
505
+ // Copying the others is not a read: the operation recorded what it read of them.
506
+ bridge.setCollections(writeCollections(untracked(bridge.getCollections()), changes.collections));
443
507
  },
444
508
  reconcile: () => {
445
509
  bridge.deriveStructure();
@@ -458,6 +522,37 @@ function writesScene(changes) {
458
522
  (toPatches(changes).length ||
459
523
  (changes.collections && Object.keys(changes.collections).length)));
460
524
  }
525
+ /**
526
+ * search_assets as the model reads it: each item's id, name and placement facts; the category
527
+ * and the floor's null attachTo stay in the structured result.
528
+ */
529
+ function searchAssetsModelResult(output) {
530
+ const groups = output.groups;
531
+ return {
532
+ ...output,
533
+ groups: groups.map((group) => ({
534
+ ...group,
535
+ results: group.results.map(({ id, name, dimensions, attachTo }) => ({
536
+ id,
537
+ name,
538
+ dimensions,
539
+ ...(attachTo && { attachTo }),
540
+ })),
541
+ })),
542
+ };
543
+ }
544
+ /**
545
+ * What the model reads of a tool's result: for verify_scene, the findings grouped and trimmed
546
+ * (`verifySceneModelResult`); for search_assets, the items without what placing them does not
547
+ * need; every other result as it is. The structured result keeps the whole.
548
+ */
549
+ function modelResult(name, output, input) {
550
+ if (name === verifySceneTool.name)
551
+ return verifySceneModelResult(output, input);
552
+ if (name === searchAssetsTool.name)
553
+ return searchAssetsModelResult(output);
554
+ return output;
555
+ }
461
556
  export function registerSharedTools(server, bridge, catalog = builtInCatalog, hosts = {}) {
462
557
  for (const tool of SHARED_TOOLS)
463
558
  registerOperationTool(server, bridge, tool, catalog, hosts);
@@ -470,7 +565,7 @@ export function registerSharedTools(server, bridge, catalog = builtInCatalog, ho
470
565
  export function registerOperationTool(server, bridge, tool, catalog = builtInCatalog, hosts = {}) {
471
566
  server.registerTool(tool.contract.name, {
472
567
  title: tool.contract.title,
473
- description: tool.contract.description,
568
+ description: surfaceDescription(tool.contract, 'mcp'),
474
569
  inputSchema: z.object(tool.contract.input),
475
570
  // Loose: a client that listed the tools rejects any field the schema leaves out, and the
476
571
  // operations in core grow fields (verify_scene's guesses) that this list would miss.
@@ -478,26 +573,34 @@ export function registerOperationTool(server, bridge, tool, catalog = builtInCat
478
573
  annotations: tool.annotations,
479
574
  }, async (input) => {
480
575
  let outcome;
576
+ const nodes = bridge.getNodes();
481
577
  // A copy of the map: a host may write its own in place (the hosted bridge does), and a
482
- // "before" that grows with the call reads every creation as unchanged.
483
- const before = { ...bridge.getNodes() };
578
+ // "before" that grows with the call reads every creation as unchanged. Copying is not a
579
+ // read: on a detached scene, the operation records what it selects.
580
+ const before = { ...untracked(nodes) };
484
581
  // The agent's session begins with its first call, before anything is written.
485
582
  const first = bridge.agentSession && startOfSession(bridge.agentSession.read(), before);
486
583
  if (first)
487
584
  bridge.agentSession?.keep(first);
488
- // Gate before catalog I/O or module compilation.
585
+ // Gate before module compilation. Under an open source the gate reads the catalog's tags to
586
+ // let technical equipment and plants through; a batch's calls are checked one by one. Any
587
+ // other refused call reads nothing of the library.
489
588
  const designBrief = briefInForce(bridge);
589
+ const gateRead = designBrief?.source === 'open'
590
+ ? await readCatalog(before, tool.contract.name, input, catalog)
591
+ : undefined;
490
592
  if (designBrief !== undefined) {
491
- const refusal = furnishingRefusal(tool.contract.name, input, designBrief);
492
- if (refusal)
593
+ const refusal = furnishingRefusal(tool.contract.name, input, designBrief, {
594
+ session: bridge.agentSession?.read(),
595
+ catalog: gateRead?.catalog,
596
+ });
597
+ if (refusal) {
598
+ // A refusal can ask the session to remember what it refused (a catalog piece under an open source).
599
+ if (refusal.keep)
600
+ bridge.agentSession?.keep(refusal.keep);
493
601
  return refusalResult(refusal);
602
+ }
494
603
  }
495
- const batchPlacesInRoom = tool.contract.name === runBatchTool.name &&
496
- Array.isArray(input.calls) &&
497
- input.calls.some((call) => typeof call === 'object' &&
498
- call !== null &&
499
- 'tool' in call &&
500
- call.tool === placeInRoomTool.name);
501
604
  const batchesObjects = tool.contract.name === runBatchTool.name &&
502
605
  Array.isArray(input.calls) &&
503
606
  input.calls.some((call) => typeof call === 'object' &&
@@ -512,12 +615,10 @@ export function registerOperationTool(server, bridge, tool, catalog = builtInCat
512
615
  ...(bridge.nodeSchema && { nodeSchema: (kind) => bridge.nodeSchema(kind) }),
513
616
  session: bridge.agentSession?.read(),
514
617
  ...(designBrief !== undefined && { designBrief }),
515
- ...((tool.catalog ||
516
- batchPlacesInRoom ||
517
- assetViewsReadCatalog(tool.contract.name, input) ||
518
- briefReadsCatalog(tool.contract.name, input)) && { catalog: await catalog() }),
618
+ // What the call reads of the library: all of a list, or what it names of a source.
619
+ ...(gateRead ?? (await readCatalog(before, tool.contract.name, input, catalog))),
519
620
  ...(batchesObjects && {
520
- batchObjects: await buildBatchObjects(input.calls, hosts.buildObject ?? refuseScripts),
621
+ batchObjects: await buildBatchObjects(input.calls, hosts.buildObject ?? refuseScripts, designBrief),
521
622
  }),
522
623
  };
523
624
  const needs = assetViewNeeds(before, tool.contract.name, input, context);
@@ -526,9 +627,12 @@ export function registerOperationTool(server, bridge, tool, catalog = builtInCat
526
627
  pictures: needs.pictures,
527
628
  });
528
629
  try {
529
- outcome = tool.operation(before, input, context);
630
+ outcome = inScene(bridge, () => tool.operation(nodes, input, context));
530
631
  }
531
632
  catch (error) {
633
+ // A refusal can ask the session to remember what it refused (a catalog piece under an open source).
634
+ if (isAgentRefusal(error) && error.keep)
635
+ bridge.agentSession?.keep(error.keep);
532
636
  return refusalResult(error);
533
637
  }
534
638
  let result = outcome.result;
@@ -563,11 +667,22 @@ export function registerOperationTool(server, bridge, tool, catalog = builtInCat
563
667
  });
564
668
  if (pictures.length)
565
669
  return {
566
- content: [{ type: 'text', text: JSON.stringify(shown) }, ...pictures],
670
+ content: [
671
+ {
672
+ type: 'text',
673
+ text: JSON.stringify(modelResult(tool.contract.name, shown, input)),
674
+ },
675
+ ...pictures,
676
+ ],
567
677
  structuredContent: shown,
568
678
  };
569
679
  return {
570
- content: [{ type: 'text', text: JSON.stringify(payload) }],
680
+ content: [
681
+ {
682
+ type: 'text',
683
+ text: JSON.stringify(modelResult(tool.contract.name, payload, input)),
684
+ },
685
+ ],
571
686
  structuredContent: payload,
572
687
  };
573
688
  });
@@ -64,7 +64,7 @@ function countNodeTypes(nodes) {
64
64
  export function registerCreateHouseFromBrief(server, bridge) {
65
65
  server.registerTool('create_house_from_brief', {
66
66
  title: 'Create house from brief',
67
- description: 'High-level hosted workflow for external agents: choose a starter house from a brief, create/save/publish it, and return the editor URL. Use semantic tools afterward for exact customization.',
67
+ description: "Replace the session's scene with a starter template and save it as an unpublished draft, returning its editor URL. The template follows bedroomCount, rooms and landscaping only: garden-house for landscaping or a garden, patio or yard room, else empty-studio up to one bedroom, two-bedroom for two and garden-house for more. brief, style and constraints neither choose nor shape it; the summary repeats brief. It saves into projectId's project, or a new one named projectName or after the template; with no scene store it saves nothing. Refine it with the semantic tools afterward.",
68
68
  inputSchema: z.object(createHouseFromBriefInput),
69
69
  outputSchema: z.object(createHouseFromBriefOutput),
70
70
  annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
@@ -1,12 +1,12 @@
1
1
  import { confirmSceneViewPresentation, sceneViewNote, sceneViewPlan, } from '@pascal-app/core/agent-operations';
2
- import { refuse, viewSceneTool } from '@pascal-app/core/agent-tools';
2
+ import { refuse, surfaceDescription, viewSceneTool } from '@pascal-app/core/agent-tools';
3
3
  import { z } from 'zod';
4
4
  import { READ_ONLY_TOOL_ANNOTATIONS } from './annotations.js';
5
5
  import { refusalResult } from './errors.js';
6
6
  export function registerViewScene(server, operations, views) {
7
7
  server.registerTool(viewSceneTool.name, {
8
8
  title: viewSceneTool.title,
9
- description: viewSceneTool.description,
9
+ description: surfaceDescription(viewSceneTool, 'mcp'),
10
10
  inputSchema: z.object(viewSceneTool.input),
11
11
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
12
12
  }, async (input) => {
@@ -32,6 +32,7 @@ you're in.
32
32
 
33
33
  Open the scene in Pascal (or render it from the JSON) and you can read
34
34
  each claim directly off the geometry:
35
+
35
36
  1. **`[x, z] → world (x, 0, z)` is exact, no sign flip.** Demo A and
36
37
  Demo B both sit flat on the floor at Y = 0; their polygon vertices
37
38
  round-trip through `save_scene` / `get_scene` byte-identical to what
@@ -33,6 +33,7 @@ The agent reads the attachment as a data URI and issues a single tool call:
33
33
  ```
34
34
 
35
35
  Optional knobs:
36
+
36
37
  - `save` (default `true`) — if `false`, the response includes `graph`
37
38
  inline instead of persisting to the `SceneStore`.
38
39
  - `defaultWallThickness` (default `0.2` m) — used when the vision model
@@ -12,6 +12,7 @@ tools to propose a renovation plan grounded in real photos.
12
12
  ## The brief
13
13
 
14
14
  The user drops four photos into the chat:
15
+
15
16
  1. A floorplan PDF page (exported as PNG).
16
17
  2. A photo of the current living room.
17
18
  3. A photo of the current kitchen.
@@ -147,6 +148,7 @@ against the corresponding perimeter wall:
147
148
 
148
149
  Guided by the reference photo's analysis (bright neutrals, open plan,
149
150
  minimal furnishing), the agent proposes a single logical patch:
151
+
150
152
  - Remove the partition wall between Living and Kitchen.
151
153
  - Relocate the kitchen island further west.
152
154
  - Delete the bulky TV unit item; leave the sofa and coffee table.