diff --git a/packages/core/src/agent-tools/clear-scene.ts b/packages/core/src/agent-tools/clear-scene.ts new file mode 100644 index 0000000000..d556419e58 --- /dev/null +++ b/packages/core/src/agent-tools/clear-scene.ts @@ -0,0 +1,11 @@ +import { z } from 'zod' + +export const clearSceneTool = { + name: 'clear_scene', + title: 'Clear the project', + description: + 'Empty the project on purpose, back to the default scaffold: a site, a building and one level of the standard storey height. It is the only way to empty a project: a write that would empty it is refused (scene_wipe_blocked), since that is far more often an accident. Call it only when the person asked to start over, and say what they asked in reason.', + input: { + reason: z.string().min(1).describe('What the person asked for, in their words.'), + }, +} diff --git a/packages/core/src/agent-tools/index.ts b/packages/core/src/agent-tools/index.ts index fa51ed107b..35766d0fd4 100644 --- a/packages/core/src/agent-tools/index.ts +++ b/packages/core/src/agent-tools/index.ts @@ -1,4 +1,5 @@ import { addObjectTool, getSourceTool } from './add-object' +import { clearSceneTool } from './clear-scene' import { editCollectionTool, listCollectionsTool } from './collections' import { addColumnTool } from './columns' import { findByTypeTool } from './find-by-type' @@ -15,6 +16,7 @@ import { fitStairTool, measureStairTool } from './stairs' import { addDoorTool, addWindowTool } from './wall-openings' export * from './add-object' +export * from './clear-scene' export * from './collections' export * from './columns' export * from './find-by-type' @@ -51,4 +53,5 @@ export const AGENT_TOOL_CONTRACTS = [ findByTypeTool, editCollectionTool, listCollectionsTool, + clearSceneTool, ] as const diff --git a/packages/mcp/README.md b/packages/mcp/README.md index d9cc1f4dcf..a8b321a316 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -357,6 +357,7 @@ captured by Zundo's temporal middleware as a single undoable step. | `set_zone` | Create a zone/room polygon on a level. | `{ levelId, polygon, label, properties? }` | `{ zoneId }` | | `duplicate_level` | Copy a level as the editor does (units whose rooms are all on it included; plan references, scans and spawns left behind), above or below, shifting the floors past it. | `{ levelId, position?, name?, preset? }` | `{ newLevelId, name, floorIndex, shiftedLevelIds, copied, skipped, newNodeIds[] }` | | `delete_node` | Delete a node with everything under it, as the editor's Delete does. | `{ id }` | `{ deletedIds: [] }` | +| `clear_scene` | Empty the project on purpose, back to the default site, building and level. The only way to empty a project: a write that would leave it empty is refused (`scene_wipe_blocked`) by a store that guards against accidental wipes, and the session keeps the project as stored. | `{ reason }` | `{ cleared: { removed }, version, graphHash }` | | `undo` | Step back through temporal history. | `{ steps? }` | `{ undone: number }` | | `redo` | Step forward through temporal history. | `{ steps? }` | `{ redone: number }` | | `export_json` | Serialize the scene graph as JSON. | `{ pretty? }` | `{ json: string }` | diff --git a/packages/mcp/src/storage/types.ts b/packages/mcp/src/storage/types.ts index 6ae871e11f..1f9cdd74ee 100644 --- a/packages/mcp/src/storage/types.ts +++ b/packages/mcp/src/storage/types.ts @@ -69,6 +69,11 @@ export interface SceneSaveOptions { agentSessionId?: string /** Optional high-level operation name for presence/debug metadata. */ operation?: string + /** + * The write empties a populated project on purpose (clear_scene). A store may refuse a write that + * empties the project unless this is set, with `SceneWipeBlockedError`. + */ + allowSceneWipe?: boolean } export type SceneSaveMode = 'draft' | 'checkpoint' @@ -171,6 +176,19 @@ export class SceneInvalidError extends Error { } } +/** + * A store refused a write that would empty a populated project without `allowSceneWipe`: far more + * often an accident (a client that never finished loading, a deletion of everything) than an intent. + * Still an invalid write to a caller that only knows `SceneInvalidError`; the MCP answers it + * `scene_wipe_blocked`, naming clear_scene. + */ +export class SceneWipeBlockedError extends SceneInvalidError { + constructor(message = 'Scene wipe blocked') { + super(message) + this.name = 'SceneWipeBlockedError' + } +} + export class SceneTooLargeError extends Error { readonly code = 'too_large' as const constructor(message = 'Scene too large') { diff --git a/packages/mcp/src/tools/apply-patch.ts b/packages/mcp/src/tools/apply-patch.ts index c9b42682ce..60ca514fac 100644 --- a/packages/mcp/src/tools/apply-patch.ts +++ b/packages/mcp/src/tools/apply-patch.ts @@ -1,10 +1,11 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { isAgentRefusal } from '@pascal-app/core/agent-tools' import type { AnyNode, AnyNodeId } from '@pascal-app/core/schema' import { z } from 'zod' import type { Patch as BridgePatch } from '../bridge/scene-bridge' import type { SceneOperations } from '../operations' import { DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations' -import { ErrorCode, throwMcpError } from './errors' +import { ErrorCode, refusalResult, throwMcpError } from './errors' import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync' import { assertPatchKeepsIdentity, PatchRefusedError } from './patch-guards' import { PatchSchema } from './schemas' @@ -90,6 +91,8 @@ export function registerApplyPatch(server: McpServer, bridge: SceneOperations): isError: true, } } + // A refusal the store made (an accidental wipe) answers with its code, as the shared tools'. + if (isAgentRefusal(err)) return refusalResult(err) const msg = err instanceof Error ? err.message : String(err) throwMcpError(ErrorCode.InvalidParams, msg) } diff --git a/packages/mcp/src/tools/clear-scene.test.ts b/packages/mcp/src/tools/clear-scene.test.ts new file mode 100644 index 0000000000..63f504cbb7 --- /dev/null +++ b/packages/mcp/src/tools/clear-scene.test.ts @@ -0,0 +1,255 @@ +import { describe, expect, test } from 'bun:test' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { AGENT_TOOL_CONTRACTS } from '@pascal-app/core/agent-tools' +import type { SceneGraph } from '@pascal-app/core/clone-scene-graph' +import { WallNode } from '@pascal-app/core/schema' +import { SceneBridge } from '../bridge/scene-bridge' +import { createSceneOperations } from '../operations' +import { type SceneMeta, type SceneStore, SceneWipeBlockedError } from '../storage/types' +import { registerApplyPatch } from './apply-patch' +import { registerClearScene } from './clear-scene' +import { registerCreateWall } from './create-wall' +import { registerCreateRoom } from './room-tools' +import { registerStructureTools } from './structure-tools' +import { registerUndo } from './undo' + +// An agent that starts over clears the project on purpose; a write that would empty it by +// accident is refused, as the hosted store refuses it (the scaffold: a site, a building, a level). +const SCAFFOLD_NODE_COUNT = 4 +const isWipe = (previous: number, next: number) => + previous > SCAFFOLD_NODE_COUNT && next <= SCAFFOLD_NODE_COUNT && previous - next > 1 + +/** A store keeping one scene, which refuses an accidental wipe as a hosted store does. */ +function guardedStore(meta: SceneMeta) { + let stored: SceneGraph | null = null + const saves: { nodeCount: number; allowSceneWipe?: boolean }[] = [] + const store: SceneStore = { + backend: 'sqlite', + async save(opts) { + const previous = Object.keys(stored?.nodes ?? {}).length + const next = Object.keys(opts.graph.nodes).length + if (stored && !opts.allowSceneWipe && isWipe(previous, next)) + throw new SceneWipeBlockedError() + stored = opts.graph + saves.push({ nodeCount: next, ...(opts.allowSceneWipe ? { allowSceneWipe: true } : {}) }) + return { ...meta, version: meta.version + saves.length, nodeCount: next } + }, + async load() { + return stored ? { ...meta, graph: stored } : null + }, + async list() { + return [] + }, + async delete() { + return { deleted: false, hidden: false } + }, + async rename() { + return meta + }, + async appendSceneEvent(opts) { + return { + eventId: 1, + sceneId: opts.sceneId, + version: opts.version, + kind: opts.kind, + createdAt: new Date().toISOString(), + graph: opts.graph, + } + }, + } + return { store, saves, seed: (graph: SceneGraph) => (stored = graph) } +} + +async function houseSession() { + const bridge = new SceneBridge() + bridge.setScene({}, []) + bridge.loadDefault() + const level = Object.values(bridge.getNodes()).find((node) => node.type === 'level')! + for (const [start, end] of [ + [ + [0, 0], + [5, 0], + ], + [ + [5, 0], + [5, 4], + ], + [ + [5, 4], + [0, 4], + ], + [ + [0, 4], + [0, 0], + ], + ] as const) + bridge.createNode(WallNode.parse({ start: [...start], end: [...end] }), level.id) + const meta: SceneMeta = { + id: 'scene_house', + name: 'House', + projectId: 'project_house', + thumbnailUrl: null, + version: 1, + createdAt: '2026-10-06T00:00:00Z', + updatedAt: '2026-10-06T00:00:00Z', + ownerId: null, + sizeBytes: 0, + nodeCount: Object.keys(bridge.getNodes()).length, + } + const guarded = guardedStore(meta) + guarded.seed(bridge.exportJSON()) + const operations = createSceneOperations({ bridge, store: guarded.store }) + operations.setActiveScene(meta) + const server = new McpServer({ name: 'clear', version: '0.0.0' }) + registerClearScene(server, operations) + registerApplyPatch(server, operations) + registerCreateWall(server, operations) + registerUndo(server, operations) + const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair() + const client = new Client({ name: 'clear-client', version: '0.0.0' }) + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]) + const call = async (name: string, args: Record) => { + const result = await client.callTool({ name, arguments: args }) + const text = (result.content as { type: string; text: string }[])[0]!.text + return { isError: !!result.isError, body: JSON.parse(text) as Record } + } + return { bridge, call, guarded, meta } +} + +describe('clear_scene', () => { + test('is one of the shared agent tools, named for a person', () => { + const contract = AGENT_TOOL_CONTRACTS.find((tool) => tool.name === 'clear_scene') + expect(contract?.title).toBe('Clear the project') + expect(contract?.description).toContain('scene_wipe_blocked') + }) + + test('empties the project on purpose; the next write lands on the scaffold', async () => { + const { bridge, call, guarded, meta } = await houseSession() + // The walls and what they derived (the room, its floor and ceiling) go; the scaffold stays. + const before = Object.keys(bridge.getNodes()).length + const cleared = await call('clear_scene', { reason: 'The person asked to start over.' }) + expect(cleared.isError).toBe(false) + expect(cleared.body.cleared).toEqual({ removed: before - 3 }) + expect(guarded.saves.at(-1)).toEqual({ nodeCount: 3, allowSceneWipe: true }) + + const level = Object.values(bridge.getNodes()).find((node) => node.type === 'level')! + expect((level as { height?: number }).height).toBe(2.5) + const wall = await call('create_wall', { levelId: level.id, start: [0, 0], end: [3, 0] }) + expect(wall.isError).toBe(false) + + // A fresh session loads what was stored: the scaffold and the one new wall. + const stored = await guarded.store.load(meta.id) + const types = Object.values(stored!.graph.nodes).map((node) => (node as { type: string }).type) + expect(types.sort()).toEqual(['building', 'level', 'site', 'wall']) + }) + + test('leaves nothing to undo: an undo after a clear does not bring back an empty or old scene', async () => { + const { bridge, call, guarded } = await houseSession() + const cleared = await call('clear_scene', { reason: 'The person asked to start over.' }) + expect(cleared.isError).toBe(false) + const saves = guarded.saves.length + await call('undo', { steps: 1 }) + const types = Object.values(bridge.getNodes()).map((node) => node.type) + expect(types.sort()).toEqual(['building', 'level', 'site']) + expect(guarded.saves.length).toBe(saves) + }) + + test("keeps the project's installed plugins, in the session and in the saved scene", async () => { + const { bridge, call, guarded, meta } = await houseSession() + bridge.loadJSON({ ...bridge.exportJSON(), installedPlugins: ['pascal:sheets'] }) + const cleared = await call('clear_scene', { reason: 'The person asked to start over.' }) + expect(cleared.isError).toBe(false) + expect(bridge.exportJSON().installedPlugins).toEqual(['pascal:sheets']) + const stored = await guarded.store.load(meta.id) + expect(stored!.graph.installedPlugins).toEqual(['pascal:sheets']) + const types = Object.values(stored!.graph.nodes).map((node) => (node as { type: string }).type) + expect(types.sort()).toEqual(['building', 'level', 'site']) + }) + + test('a write that would empty the project by accident is refused, and nothing changed', async () => { + const { bridge, call, guarded } = await houseSession() + const before = Object.keys(bridge.getNodes()).sort() + const site = bridge.getRootNodeIds()[0]! + const refused = await call('apply_patch', { patches: [{ op: 'delete', id: site }] }) + expect(refused.isError).toBe(true) + expect(refused.body.code).toBe('scene_wipe_blocked') + expect(String(refused.body.error)).toContain('nothing changed') + expect(String(refused.body.error)).toContain('clear_scene') + expect(guarded.saves).toEqual([]) + // The session holds the project as stored, not the refused write. + expect(Object.keys(bridge.getNodes()).sort()).toEqual(before) + }) +}) + +// An agent deletes the only room it built: the store refuses the write as a wipe, and the session +// must not keep the deletion, or its next writes build on a scene the project does not hold. +describe('deleting the only room', () => { + async function oneRoomSession() { + const bridge = new SceneBridge() + bridge.setScene({}, []) + bridge.loadDefault() + const meta: SceneMeta = { + id: 'scene_room', + name: 'Room', + projectId: 'project_room', + thumbnailUrl: null, + version: 1, + createdAt: '2026-10-06T00:00:00Z', + updatedAt: '2026-10-06T00:00:00Z', + ownerId: null, + sizeBytes: 0, + nodeCount: Object.keys(bridge.getNodes()).length, + } + const guarded = guardedStore(meta) + guarded.seed(bridge.exportJSON()) + const operations = createSceneOperations({ bridge, store: guarded.store }) + operations.setActiveScene(meta) + const server = new McpServer({ name: 'room', version: '0.0.0' }) + registerCreateRoom(server, operations) + registerStructureTools(server, operations) + registerCreateWall(server, operations) + const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair() + const client = new Client({ name: 'room-client', version: '0.0.0' }) + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]) + const call = async (name: string, args: Record) => { + const result = await client.callTool({ name, arguments: args }) + return { + isError: !!result.isError, + text: (result.content as { type: string; text: string }[])[0]!.text, + } + } + const level = Object.values(bridge.getNodes()).find((node) => node.type === 'level')! + const room = await call('create_room', { + levelId: level.id, + name: 'Studio', + polygon: [ + [0, 0], + [4, 0], + [4, 3], + [0, 3], + ], + }) + expect(room.isError).toBe(false) + return { bridge, call, guarded, meta, levelId: level.id, zoneId: JSON.parse(room.text).zoneId } + } + + test('is refused as a wipe, the session keeps the room, and the next write matches the store', async () => { + const { bridge, call, guarded, meta, levelId, zoneId } = await oneRoomSession() + const before = Object.keys(bridge.getNodes()).sort() + const saves = guarded.saves.length + const refused = await call('delete_zone', { zoneId, contents: 'delete' }) + expect(refused.isError).toBe(true) + expect(refused.text).toContain('nothing changed') + expect(refused.text).toContain('only room') + expect(guarded.saves.length).toBe(saves) + expect(Object.keys(bridge.getNodes()).sort()).toEqual(before) + + const wall = await call('create_wall', { levelId, start: [6, 0], end: [8, 0] }) + expect(wall.isError).toBe(false) + const stored = await guarded.store.load(meta.id) + expect(Object.keys(stored!.graph.nodes).sort()).toEqual(Object.keys(bridge.getNodes()).sort()) + expect(Object.keys(stored!.graph.nodes)).toContain(zoneId) + }) +}) diff --git a/packages/mcp/src/tools/clear-scene.ts b/packages/mcp/src/tools/clear-scene.ts new file mode 100644 index 0000000000..eb9c5e487f --- /dev/null +++ b/packages/mcp/src/tools/clear-scene.ts @@ -0,0 +1,65 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { clearSceneTool } from '@pascal-app/core/agent-tools' +import { z } from 'zod' +import type { SceneOperations } from '../operations' +import { DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations' +import { refusalResult } from './errors' +import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './live-sync' + +/** + * `clear_scene`: the scene goes back to the host's default scaffold (a site, a building, a level), + * then is saved with `allowSceneWipe`, so a store that refuses an accidental wipe takes this one. + * The project's installed plugins stay, and its undo history goes, as the editor's own clear does. + */ +export function registerClearScene(server: McpServer, operations: SceneOperations): void { + server.registerTool( + clearSceneTool.name, + { + title: clearSceneTool.title, + description: clearSceneTool.description, + inputSchema: clearSceneTool.input, + outputSchema: { + cleared: z.object({ removed: z.number() }), + version: z.number().nullable(), + graphHash: z.string().nullable(), + ...liveSyncOutput, + }, + annotations: DESTRUCTIVE_TOOL_ANNOTATIONS, + }, + async () => { + try { + const before = Object.keys(operations.getNodes()).length + const { installedPlugins } = operations.exportSceneGraph() + operations.setScene({}, []) + operations.loadDefault() + // Resetting drops the plugin state on every host; the scaffold is applied again with it. + if (installedPlugins) + operations.loadJSON({ + ...operations.exportSceneGraph(), + collections: {}, + materials: {}, + installedPlugins, + }) + // Nothing to undo back into: the empty intermediate or the old scene, saved over the clear. + operations.clearHistory() + const removed = Math.max(0, before - Object.keys(operations.getNodes()).length) + const persistence = await publishLiveSceneSnapshot(operations, clearSceneTool.name, { + allowSceneWipe: true, + }) + const active = operations.getActiveScene() + const payload = { + cleared: { removed }, + version: active?.version ?? null, + graphHash: active?.graphHash ?? null, + ...persistencePayload(persistence), + } + return { + content: [{ type: 'text' as const, text: JSON.stringify(payload) }], + structuredContent: payload, + } + } catch (error) { + return refusalResult(error) + } + }, + ) +} diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts index 2ea78af920..25f7712d46 100644 --- a/packages/mcp/src/tools/index.ts +++ b/packages/mcp/src/tools/index.ts @@ -4,6 +4,7 @@ import { registerAddColumn } from './add-column' import { type GeometryScriptHost, registerAddObject, registerGetSource } from './add-object' import { registerApplyPatch } from './apply-patch' import { registerCheckCollisions } from './check-collisions' +import { registerClearScene } from './clear-scene' import { registerConstructionTools } from './construction-tools' import { registerCreateLevel } from './create-level' import { registerCreateUnit } from './create-unit' @@ -56,6 +57,7 @@ export function registerTools( registerConstructionTools(server, operations) registerRoomTools(server, operations, geometryScripts) registerApplyPatch(server, operations) + registerClearScene(server, operations) registerCreateLevel(server, operations) registerCreateUnit(server, operations) registerSetUnitMembers(server, operations) diff --git a/packages/mcp/src/tools/live-sync.ts b/packages/mcp/src/tools/live-sync.ts index dc564a327a..a45c1155be 100644 --- a/packages/mcp/src/tools/live-sync.ts +++ b/packages/mcp/src/tools/live-sync.ts @@ -1,7 +1,8 @@ +import { refuse } from '@pascal-app/core/agent-tools' import type { SceneGraph } from '@pascal-app/core/clone-scene-graph' import { z } from 'zod' import type { SceneOperations } from '../operations' -import { SceneVersionConflictError } from '../storage/types' +import { SceneVersionConflictError, SceneWipeBlockedError } from '../storage/types' import { ErrorCode, McpError, throwMcpError } from './errors' export type LiveSyncStatus = 'published' | 'unbound' | 'events_unsupported' @@ -60,6 +61,8 @@ export function isLiveSyncVersionConflict(error: unknown): boolean { export async function publishLiveSceneSnapshot( operations: SceneOperations, kind: string, + /** `allowSceneWipe`: the write empties the project on purpose (clear_scene). */ + options: { allowSceneWipe?: boolean } = {}, ): Promise { const active = operations.getActiveScene() if (!active) return 'unbound' @@ -80,6 +83,7 @@ export async function publishLiveSceneSnapshot( saveMode: 'draft', publish: false, operation: kind, + ...(options.allowSceneWipe ? { allowSceneWipe: true } : {}), }) operations.setActiveScene(meta) await operations.appendSceneEvent({ @@ -89,6 +93,19 @@ export async function publishLiveSceneSnapshot( graph, }) } catch (error) { + if (error instanceof SceneWipeBlockedError) { + // The store kept what it held; the session goes back to it, so the agent's next write builds + // on the project as stored rather than on the refused one (deleting the only room, say). + const stored = await operations.loadStoredScene(active.id).catch(() => null) + if (stored) operations.loadJSON(stored.graph) + refuse( + 'scene_wipe_blocked', + stored + ? 'This write would leave the project empty, so it was blocked and nothing changed. To empty the project on purpose, call clear_scene. To remove only part of it, such as its only room, build what replaces it first, then remove it.' + : 'This write would leave the project empty, so it was blocked and not saved. Call load_scene before writing again. To empty the project on purpose, call clear_scene.', + { sceneId: active.id, mutationApplied: false, sessionRestored: !!stored }, + ) + } if (error instanceof SceneVersionConflictError) { throwMcpError(ErrorCode.InvalidRequest, LIVE_SYNC_VERSION_CONFLICT, { sceneId: active.id, diff --git a/packages/mcp/src/tools/read-tool-annotations.test.ts b/packages/mcp/src/tools/read-tool-annotations.test.ts index 70401bc8a8..62c2de9fbb 100644 --- a/packages/mcp/src/tools/read-tool-annotations.test.ts +++ b/packages/mcp/src/tools/read-tool-annotations.test.ts @@ -89,6 +89,7 @@ const TOOL_POLICIES = [ }, tools: [ 'apply_patch', + 'clear_scene', 'add_object', 'add_column', 'edit_collection', diff --git a/plugin-evals/tool-annotation-justifications.json b/plugin-evals/tool-annotation-justifications.json index 9bf43e57fe..577a4cd7e9 100644 --- a/plugin-evals/tool-annotation-justifications.json +++ b/plugin-evals/tool-annotation-justifications.json @@ -110,6 +110,19 @@ "openWorldHint": "It operates only on data supplied by or already available inside the connected Pascal service and does not access the public internet." } }, + { + "name": "clear_scene", + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "openWorldHint": false + }, + "justifications": { + "readOnlyHint": "It is not read-only because it empties the project back to its default scaffold and saves it.", + "destructiveHint": "The operation removes everything in the project except the site, building and level, on purpose.", + "openWorldHint": "It operates on the connected Pascal scene or project service without accessing the public internet." + } + }, { "name": "create_from_template", "annotations": { diff --git a/scripts/openai-tool-annotation-policy.test.ts b/scripts/openai-tool-annotation-policy.test.ts index 4d05ad6fcd..b1b3bdc534 100644 --- a/scripts/openai-tool-annotation-policy.test.ts +++ b/scripts/openai-tool-annotation-policy.test.ts @@ -13,7 +13,7 @@ describe('OpenAI tool annotation justification packet', () => { const candidate = clonePacket() candidate.tools.pop() expect(validateOpenAiToolAnnotationPacket(candidate)).toContain( - 'OpenAI tool annotation packet must contain the exact 73-tool inventory', + 'OpenAI tool annotation packet must contain the exact 74-tool inventory', ) }) diff --git a/scripts/openai-tool-annotation-policy.ts b/scripts/openai-tool-annotation-policy.ts index ec906ecf8b..4afce8a189 100644 --- a/scripts/openai-tool-annotation-policy.ts +++ b/scripts/openai-tool-annotation-policy.ts @@ -26,6 +26,7 @@ export const EXPECTED_OPENAI_TOOL_ANNOTATIONS = { edit_collection: policy(false, true, false), list_collections: policy(true, false, false), check_collisions: policy(true, false, false), + clear_scene: policy(false, true, false), create_from_template: policy(false, true, false), create_house_from_brief: policy(false, true, false), create_mezzanine: policy(false, false, false),