From 42290a91b097d2e04c5a1998386d8db02d6d02dd Mon Sep 17 00:00:00 2001 From: AfreedHassan Date: Tue, 16 Sep 2025 20:25:14 -0400 Subject: [PATCH 1/2] updated .gitignore to ignore .env files --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0a0cfdd..6094fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ node_modules/ .next/ # Env files -warp/apps/web/.env -warp/apps/web/.env.local -warp/apps/web/.env.* +vector/apps/web/.env +vector/apps/web/.env.local +vector/apps/web/.env.* # Local data store data/ From 1860af382543aec82d47de7eee26dc21e39287ef Mon Sep 17 00:00:00 2001 From: AfreedHassan Date: Tue, 16 Sep 2025 20:25:41 -0400 Subject: [PATCH 2/2] fixed SYSTEM_PROMPT and formatting of orchestrator/index.ts --- vector/apps/web/lib/orchestrator/index.ts | 1214 +++++++++++++-------- 1 file changed, 789 insertions(+), 425 deletions(-) diff --git a/vector/apps/web/lib/orchestrator/index.ts b/vector/apps/web/lib/orchestrator/index.ts index cf0d745..0075360 100644 --- a/vector/apps/web/lib/orchestrator/index.ts +++ b/vector/apps/web/lib/orchestrator/index.ts @@ -1,417 +1,689 @@ -export type EditPos = { line: number; character: number } -export type Edit = { start: EditPos; end: EditPos; text: string } +export type EditPos = { line: number; character: number }; +export type Edit = { start: EditPos; end: EditPos; text: string }; export type EditProposal = { - id: string - type: 'edit' - path: string - diff: { mode: 'rangeEDITS'; edits: Edit[] } - notes?: string - safety?: { beforeHash?: string } -} + id: string; + type: "edit"; + path: string; + diff: { mode: "rangeEDITS"; edits: Edit[] }; + notes?: string; + safety?: { beforeHash?: string }; +}; export type ObjectOp = - | { op: 'create_instance'; className: string; parentPath: string; props?: Record } - | { op: 'set_properties'; path: string; props: Record } - | { op: 'rename_instance'; path: string; newName: string } - | { op: 'delete_instance'; path: string } -export type ObjectProposal = { id: string; type: 'object_op'; ops: ObjectOp[]; notes?: string } + | { + op: "create_instance"; + className: string; + parentPath: string; + props?: Record; + } + | { op: "set_properties"; path: string; props: Record } + | { op: "rename_instance"; path: string; newName: string } + | { op: "delete_instance"; path: string }; +export type ObjectProposal = { + id: string; + type: "object_op"; + ops: ObjectOp[]; + notes?: string; +}; export type AssetProposal = { - id: string - type: 'asset_op' - search?: { query: string; tags?: string[]; limit?: number } - insert?: { assetId: number; parentPath?: string } - generate3d?: { prompt: string; tags?: string[]; style?: string; budget?: number } -} -export type Proposal = EditProposal | ObjectProposal | AssetProposal + id: string; + type: "asset_op"; + search?: { query: string; tags?: string[]; limit?: number }; + insert?: { assetId: number; parentPath?: string }; + generate3d?: { + prompt: string; + tags?: string[]; + style?: string; + budget?: number; + }; +}; +export type Proposal = EditProposal | ObjectProposal | AssetProposal; export type ChatInput = { - projectId: string - message: string + projectId: string; + message: string; context: { // activeScript can be undefined when Studio has no open script; provider can gather context via tools - activeScript?: { path: string; text: string } | null - selection?: { className: string; path: string }[] - openDocs?: { path: string }[] - } - provider?: { name: 'openrouter'; apiKey: string; model?: string; baseUrl?: string } - mode?: 'ask' | 'agent' - maxTurns?: number - enableFallbacks?: boolean -} - -function id(prefix = 'p'): string { - return `${prefix}_${Math.random().toString(36).slice(2, 8)}_${Date.now().toString(36)}` + activeScript?: { path: string; text: string } | null; + selection?: { className: string; path: string }[]; + openDocs?: { path: string }[]; + }; + provider?: { + name: "openrouter"; + apiKey: string; + model?: string; + baseUrl?: string; + }; + mode?: "ask" | "agent"; + maxTurns?: number; + enableFallbacks?: boolean; +}; + +function id(prefix = "p"): string { + return `${prefix}_${Math.random().toString(36).slice(2, 8)}_${Date.now().toString(36)}`; } function sanitizeComment(text: string): string { - return text.replace(/\n/g, ' ').slice(0, 160) + return text.replace(/\n/g, " ").slice(0, 160); } // Provider call -import { callOpenRouter } from './providers/openrouter' -import { z } from 'zod' -import { Tools } from '../tools/schemas' -import { getSession, setLastTool } from '../store/sessions' -import { pushChunk } from '../store/stream' -import { applyRangeEdits, simpleUnifiedDiff } from '../diff/rangeEdits' -import crypto from 'node:crypto' +import { callOpenRouter } from "./providers/openrouter"; +import { z } from "zod"; +import { Tools } from "../tools/schemas"; +import { getSession, setLastTool } from "../store/sessions"; +import { pushChunk } from "../store/stream"; +import { applyRangeEdits, simpleUnifiedDiff } from "../diff/rangeEdits"; +import crypto from "node:crypto"; const SYSTEM_PROMPT = `You are Vector, a Roblox Studio copilot. - -Core rules -- One tool per turn: emit EXACTLY ONE tool tag and NOTHING ELSE. Wait for the tool result before the next step. -- Proposal-first and undoable: never change code/Instances directly; always propose a small, safe step the plugin can preview/apply. -- No prose, no markdown, no code fences, no extra tags. Do NOT invent fictitious tags like or . - -Tool call format (XML-like) -\n ...\n ...\n - -Encoding for parameters -- Strings/numbers: write the literal value. -- Objects/arrays: INNER TEXT MUST be strict JSON (double quotes; no trailing commas). Never wrap JSON in quotes. Never add code fences. - ✅ {"Name":"Grid","Anchored":true} - ❌ "{ \"Name\": \"Grid\" }" - ❌ ```json{\n \"Name\": \"Grid\"\n}``` - ❌ { Name: "Grid", } -- If a parameter is optional and unknown, omit the tag entirely (do NOT write "null" or "undefined"). - -Available tools -- Context (read-only): get_active_script(), list_selection(), list_open_documents(). -- Actions: show_diff(path,edits[]), apply_edit(path,edits[]), create_instance(className,parentPath,props?), set_properties(path,props), rename_instance(path,newName), delete_instance(path), search_assets(query,tags?,limit?), insert_asset(assetId,parentPath?), generate_asset_3d(prompt,tags?,style?,budget?). - -Paths & names -- Use canonical GetFullName() paths, e.g., game.Workspace.Model.Part. -- Avoid creating names with dots or slashes; prefer alphanumerics + underscores (e.g., Cell_1_1). -- If an existing path contains special characters, bracket segments: game.Workspace["My.Part"]["Wall [A]"] - -Roblox typed values (for props) -- Scalars/booleans/strings: raw JSON. -- Wrappers with "__t": Vector3 {"__t":"Vector3","x":0,"y":1,"z":0}; Vector2 {"__t":"Vector2","x":0,"y":0}; - Color3 {"__t":"Color3","r":1,"g":0.5,"b":0.25}; UDim {"__t":"UDim","scale":0,"offset":16}; - UDim2 {"__t":"UDim2","x":{"scale":0,"offset":0},"y":{"scale":0,"offset":0}}; - CFrame {"__t":"CFrame","comps":[x,y,z, r00,r01,r02, r10,r11,r12, r20,r21,r22]}; - EnumItem {"__t":"EnumItem","enum":"Enum.Material","name":"Plastic"}; - BrickColor {"__t":"BrickColor","name":"Bright red"}; - Instance ref {"__t":"Instance","path":"game.ReplicatedStorage.Folder.Template"}. -- Attributes: prefix keys with @, e.g., {"@Health":100}. - -Editing rules -- 0-based coordinates; end is exclusive. Prefer the smallest edit set; avoid whole-file rewrites. -- Prefer show_diff first; use apply_edit only after approval. Never include __finalText. - -Context & defaults -- If you need path/selection and it wasn't provided, call get_active_script / list_selection first. -- If parentPath is unknown for create_instance/insert_asset, use game.Workspace. -- Only set real properties for the target class; do not create instances via set_properties. - -Modes -- Ask: do exactly one atomic change. -- Agent: fetch minimal context if needed, then act with one tool. -- Auto: assume approved small steps; avoid destructive ops; skip actions that require human choice. - -Assets & 3D -- search_assets: keep limit ≤ 6 unless the user asks; include tags when helpful. -- insert_asset: assetId must be a number; default parentPath to game.Workspace if unknown. -- generate_asset_3d: returns a jobId only; prefer insert_asset when inserting existing assets. - -Validation & recovery -- On VALIDATION_ERROR, resubmit the SAME tool once with corrected args; no commentary and no tool switching. - -Selection defaults -- If EXACTLY ONE instance is selected and it is a reasonable container, prefer it by default: - - Use that selection as for create_instance/insert_asset when unspecified. - - Use that selection's path for rename_instance/set_properties/delete_instance when is missing. - - If no single valid selection, default to game.Workspace or fetch context as needed. - -Properties vs Attributes -- Only set real class properties in . If a key is not a property of the target class, write it as an Attribute by prefixing with "@". -- Do NOT rename via set_properties. Use instead. - -Edit constraints -- Edits must be sorted by start position and be NON-OVERLAPPING. -- Keep small: ≤ 20 edits AND ≤ 2000 inserted characters total. -- Never send an empty edits array. - -Safety for instance ops -- Never delete DataModel or Services. Operate under Workspace unless the user targets another service explicitly. -- Use only valid Roblox class names for . If unsure, prefer "Part" or "Model". - -Path & JSON hygiene -- No leading/trailing whitespace or code fences inside parameter bodies. -- Do not include blank lines before/after the outer tool tag. -- When a segment contains dots/brackets, use bracket notation exactly: game.Workspace["My.Part"]["Wall [A]"] - -Examples (correct) -\n Part\n game.Workspace\n {"Name":"Cell_1_1","Anchored":true,"Size":{"__t":"Vector3","x":4,"y":1,"z":4},"CFrame":{"__t":"CFrame","comps":[0,0.5,0, 1,0,0, 0,1,0, 0,0,1]}}\n - -\n game.Workspace.Cell_1_1\n {"Anchored":true,"Material":{"__t":"EnumItem","enum":"Enum.Material","name":"Plastic"},"@Health":100}\n - -\n game.Workspace.Script\n [{"start":{"line":0,"character":0},"end":{"line":0,"character":0},"text":"-- Header\\n"}]\n` + Core rules + - One tool per turn: emit EXACTLY ONE tool tag and NOTHING ELSE. Wait for the tool result before the next step. + - Proposal-first and undoable: never change code/Instances directly; always propose a small, safe step the plugin + can preview/apply. + - No prose, no markdown, no code fences, no extra tags. Do NOT invent fictitious tags like or . + Tool call format (XML-like) + \n ...\n ...\n + + Encoding for parameters + - Strings/numbers: write the literal value. + - Objects/arrays: INNER TEXT MUST be strict JSON (double quotes; no trailing commas). Never wrap JSON in quotes. + Never add code fences. + ✅ {"Name":"Grid","Anchored":true} + ❌ "{ \"Name\": \"Grid\" }" + ❌ \`\`\`json{ \n \"Name\": \"Grid\"\n}\`\`\` + ❌ { Name: "Grid", } + - If a parameter is optional and unknown, omit the tag entirely(do NOT write "null" or "undefined"). + + Available tools + - Context(read - only): get_active_script(), list_selection(), list_open_documents(). + - Actions: show_diff(path, edits[]), apply_edit(path, edits[]), create_instance(className, parentPath, props ?), + set_properties(path, props), rename_instance(path, newName), delete_instance(path), search_assets(query, tags ?, + limit ?), insert_asset(assetId, parentPath ?), generate_asset_3d(prompt, tags ?, style ?, budget ?). + + Paths & names + - Use canonical GetFullName() paths, e.g., game.Workspace.Model.Part. + - Avoid creating names with dots or slashes; prefer alphanumerics + underscores(e.g., Cell_1_1). + - If an existing path contains special characters, bracket segments: game.Workspace["My.Part"]["Wall [A]"] + + Roblox typed values(for props) + - Scalars / booleans / strings: raw JSON. + - Wrappers with "__t": Vector3 { "__t": "Vector3", "x": 0, "y": 1, "z": 0 }; Vector2 { "__t": "Vector2", "x": 0, + "y": 0 }; + Color3 { "__t": "Color3", "r": 1, "g": 0.5, "b": 0.25 }; UDim { "__t": "UDim", "scale": 0, "offset": 16 }; + UDim2 { "__t": "UDim2", "x": { "scale": 0, "offset": 0 }, "y": { "scale": 0, "offset": 0 } }; + CFrame { "__t": "CFrame", "comps": [x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22] }; + EnumItem { "__t": "EnumItem", "enum": "Enum.Material", "name": "Plastic" }; + BrickColor { "__t": "BrickColor", "name": "Bright red" }; + Instance ref { "__t": "Instance", "path": "game.ReplicatedStorage.Folder.Template" }. + - Attributes: prefix keys with @, e.g., { "@Health": 100 }. + + Editing rules + - 0 - based coordinates; end is exclusive.Prefer the smallest edit set; avoid whole - file rewrites. + - Prefer show_diff first; use apply_edit only after approval.Never include __finalText. + + Context & defaults + - If you need path / selection and it wasn't provided, call get_active_script / list_selection first. + - If parentPath is unknown for create_instance / insert_asset, use game.Workspace. + - Only set real properties for the target class; do not create instances via set_properties. + + Modes + - Ask: do exactly one atomic change. + - Agent: fetch minimal context if needed, then act with one tool. + - Auto: assume approved small steps; avoid destructive ops; skip actions that require human choice. + + Assets & 3D + - search_assets: keep limit ≤ 6 unless the user asks; include tags when helpful. + - insert_asset: assetId must be a number; default parentPath to game.Workspace if unknown. + - generate_asset_3d: returns a jobId only; prefer insert_asset when inserting existing assets. + + Validation & recovery + - On VALIDATION_ERROR, resubmit the SAME tool once with corrected args; no commentary and no tool switching. + + Selection defaults + - If EXACTLY ONE instance is selected and it is a reasonable container, prefer it by default: + - Use that selection as for create_instance / insert_asset when unspecified. + - Use that selection's path for rename_instance/set_properties/delete_instance when is missing. + - If no single valid selection, default to game.Workspace or fetch context as needed. + + Properties vs Attributes + - Only set real class properties in .If a key is not a property of the target class, write it as an + Attribute by prefixing with "@". + - Do NOT rename via set_properties.Use < rename_instance> instead. + + Edit constraints + - Edits must be sorted by start position and be NON - OVERLAPPING. + - Keep small: ≤ 20 edits AND ≤ 2000 inserted characters total. + - Never send an empty edits array. + + Safety for instance ops + - Never delete DataModel or Services.Operate under Workspace unless the user targets another service + explicitly. + - Use only valid Roblox class names for .If unsure, prefer "Part" or "Model". + + Path & JSON hygiene + - No leading / trailing whitespace or code fences inside parameter bodies. + - Do not include blank lines before / after the outer tool tag. + - When a segment contains dots / brackets, use bracket notation exactly: + game.Workspace["My.Part"]["Wall [A]"] + + Examples(correct) + \n < className> Part < /className>\n game.Workspace\n < + props> { "Name": "Cell_1_1", "Anchored": true, "Size": { "__t": "Vector3", "x": 4, "y": 1, + "z": 4 }, "CFrame": { "__t": "CFrame", "comps": [0, 0.5, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] } } < + /props>\n + + \n < path> game.Workspace.Cell_1_1 < /path>\n + {"Anchored":true,"Material":{"__t":"EnumItem","enum":"Enum.Material","name":"Plastic"},"@Health":100} + \n + + \n < path> game.Workspace.Script < /path>\n + [{"start":{"line":0,"character":0},"end":{"line":0,"character":0},"text":"-- Header\\n"}] + \n `; function tryParseJSON(s: unknown): T | undefined { - if (typeof s !== 'string') return undefined - const t = s.trim() - if (!t) return undefined - if (t.startsWith('{') || t.startsWith('[')) { - try { return JSON.parse(t) as T } catch { return undefined } + if (typeof s !== "string") return undefined; + const t = s.trim(); + if (!t) return undefined; + if (t.startsWith("{") || t.startsWith("[")) { + try { + return JSON.parse(t) as T; + } catch { + return undefined; + } } - return undefined + return undefined; } function tryParseStrictJSON(s: string): any { - try { return JSON.parse(s) } catch { return undefined } + try { + return JSON.parse(s); + } catch { + return undefined; + } } function coercePrimitive(v: string): any { - const t = v.trim() - if (t === 'true') return true - if (t === 'false') return false - if (t === 'null') return null - if (!isNaN(Number(t))) return Number(t) + const t = v.trim(); + if (t === "true") return true; + if (t === "false") return false; + if (t === "null") return null; + if (!isNaN(Number(t))) return Number(t); // Try strict JSON first - const jStrict = tryParseStrictJSON(t) - if (jStrict !== undefined) return jStrict - // JSON5-like fallback for common LLM outputs: single quotes, unquoted keys, trailing commas, fenced code - if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) { - let s = t + const jStrict = tryParseStrictJSON(t); + if (jStrict !== undefined) return jStrict; + // JSON5-like fallback for common LLM outputs: single quotes, unquoted keys, trailing commas, fenced + // code + if ( + (t.startsWith("{") && t.endsWith("}")) || + (t.startsWith("[") && t.endsWith("]")) + ) { + let s = t; // Remove surrounding code fences/backticks if present - s = s.replace(/^```(?:json)?/i, '').replace(/```$/i, '') + s = s.replace(/^```(?:json)?/i, "").replace(/```$/i, ""); // Replace single-quoted strings with double quotes - s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"') - // Quote bare keys: {key: -> {"key": and , key: -> , "key": - s = s.replace(/([\{,]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":') + s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"'); + // Quote bare keys: {key: -> {"key": and , key: -> , "key": + s = s.replace(/([\{,]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":'); // Remove trailing commas before } or ] - s = s.replace(/,\s*([}\]])/g, '$1') - const jLoose = tryParseStrictJSON(s) - if (jLoose !== undefined) return jLoose + s = s.replace(/,\s*([}\]])/g, "$1"); + const jLoose = tryParseStrictJSON(s); + if (jLoose !== undefined) return jLoose; } - return v + return v; } -function parseToolXML(text: string): { name: string; args: Record } | null { - if (!text) return null - const toolMatch = text.match(/<([a-zA-Z_][\w]*)>([\s\S]*)<\/\1>/) - if (!toolMatch) return null - const name = toolMatch[1] - const inner = toolMatch[2] +function parseToolXML( + text: string, +): { name: string; args: Record } | null { + if (!text) return null; + + const toolMatch = text.match(/<([a-za-z_][\w]*)>([\s\s]*)<\/\1>/); //prettier - ignore + if (!toolMatch) return null; + const name = toolMatch[1]; + const inner = toolMatch[2]; // parse child tags into args - const args: Record = {} - const tagRe = /<([a-zA-Z_][\w]*)>([\s\S]*?)<\/\1>/g - let m: RegExpExecArray | null + const args: Record = {}; + const tagRe = /<([a-zA-Z_][\w]*)>([\s\S]*?)<\/\1>/g; //prettier - ignore + let m: RegExpExecArray | null; while ((m = tagRe.exec(inner))) { - const k = m[1] - const raw = m[2] - args[k] = coercePrimitive(raw) + const k = m[1]; + const raw = m[2]; + args[k] = coercePrimitive(raw); } // If no child tags, try parsing whole inner as JSON (or JSON-like) if (Object.keys(args).length === 0) { - const asJson = coercePrimitive(inner) - if (asJson && typeof asJson === 'object') return { name, args: asJson as any } + const asJson = coercePrimitive(inner); + if (asJson && typeof asJson === "object") + return { name, args: asJson as any }; } - return { name, args } + return { name, args }; } function toEditArray(editsRaw: any): Edit[] | null { - const parsed = Array.isArray(editsRaw) ? editsRaw : tryParseJSON(editsRaw) - if (!parsed || !Array.isArray(parsed) || parsed.length === 0) return null - const out: Edit[] = [] + const parsed = Array.isArray(editsRaw) ? editsRaw : tryParseJSON(editsRaw); + if (!parsed || !Array.isArray(parsed) || parsed.length === 0) return null; + + const out: Edit[] = []; + for (const e of parsed) { if ( - e && e.start && e.end && typeof e.text === 'string' && - typeof e.start.line === 'number' && typeof e.start.character === 'number' && - typeof e.end.line === 'number' && typeof e.end.character === 'number' + e && + e.start && + e.end && + typeof e.text === "string" && + typeof e.start.line === "number" && + typeof e.start.character === "number" && + typeof e.end.line === "number" && + typeof e.end.character === "number" ) { - out.push({ start: { line: e.start.line, character: e.start.character }, end: { line: e.end.line, character: e.end.character }, text: e.text }) + out.push({ + start: { line: e.start.line, character: e.start.character }, + end: { line: e.end.line, character: e.end.character }, + text: e.text, + }); } } - if (!out.length) return null + + if (!out.length) return null; + // sort by start then end - out.sort((a, b) => - (a.start.line - b.start.line) || - (a.start.character - b.start.character) || - (a.end.line - b.end.line) || - (a.end.character - b.end.character) - ) + out.sort( + (a, b) => + a.start.line - b.start.line || + a.start.character - b.start.character || + a.end.line - b.end.line || + a.end.character - b.end.character, + ); + // ensure non-overlapping ranges for (let i = 1; i < out.length; i++) { - const prev = out[i - 1] - const cur = out[i] - const overlaps = cur.start.line < prev.end.line || ( - cur.start.line === prev.end.line && cur.start.character < prev.end.character - ) - if (overlaps) return null + const prev = out[i - 1]; + const cur = out[i]; + const overlaps = + cur.start.line < prev.end.line || + (cur.start.line === prev.end.line && + cur.start.character < prev.end.character); + + if (overlaps) return null; } + // budget caps: max 20 edits, 2000 inserted chars - const totalInsertChars = out.reduce((n, e) => n + (e.text ? String(e.text).length : 0), 0) - if (out.length > 20 || totalInsertChars > 2000) return null - return out + const totalInsertChars = out.reduce( + (n, e) => n + (e.text ? String(e.text).length : 0), + 0, + ); + if (out.length > 20 || totalInsertChars > 2000) return null; + + return out; } -function mapToolToProposals(name: string, a: Record, input: ChatInput, msg: string): Proposal[] { - const proposals: Proposal[] = [] +function mapToolToProposals( + name: string, + a: Record, + input: ChatInput, + msg: string, +): Proposal[] { + const proposals: Proposal[] = []; + const ensurePath = (fallback?: string | null): string | undefined => { - const p = typeof a.path === 'string' ? a.path : undefined - return p || (fallback || undefined) - } - if (name === 'show_diff' || name === 'apply_edit') { - const path = ensurePath(input.context.activeScript?.path || null) - const edits = toEditArray((a as any).edits) + const p = typeof a.path === "string" ? a.path : undefined; + return p || fallback || undefined; + }; + + if (name === "show_diff" || name === "apply_edit") { + const path = ensurePath(input.context.activeScript?.path || null); + const edits = toEditArray((a as any).edits); if (path && edits) { - const old = input.context.activeScript?.text || '' - const next = applyRangeEdits(old, edits) - const unified = simpleUnifiedDiff(old, next, path) - const beforeHash = crypto.createHash('sha1').update(old).digest('hex') - proposals.push({ id: id('edit'), type: 'edit', path, notes: `Parsed from ${name}`, diff: { mode: 'rangeEDITS', edits }, preview: { unified }, safety: { beforeHash } } as any) - return proposals + const old = input.context.activeScript?.text || ""; + const next = applyRangeEdits(old, edits); + const unified = simpleUnifiedDiff(old, next, path); + const beforeHash = crypto.createHash("sha1").update(old).digest("hex"); + proposals.push({ + id: id("edit"), + type: "edit", + path, + notes: `Parsed from ${name}`, + diff: { mode: "rangeEDITS", edits }, + preview: { unified }, + safety: { beforeHash }, + } as any); + return proposals; } } - if (name === 'create_instance') { - const parentPath: string | undefined = (a as any).parentPath - if (typeof (a as any).className === 'string' && parentPath) { - const op: ObjectOp = { op: 'create_instance', className: (a as any).className, parentPath, props: (a as any).props } - proposals.push({ id: id('obj'), type: 'object_op', ops: [op], notes: 'Parsed from create_instance' }) - return proposals + + if (name === "create_instance") { + const parentPath: string | undefined = (a as any).parentPath; + if (typeof (a as any).className === "string" && parentPath) { + const op: ObjectOp = { + op: "create_instance", + className: (a as any).className, + parentPath, + props: (a as any).props, + }; + proposals.push({ + id: id("obj"), + type: "object_op", + ops: [op], + notes: "Parsed from create_instance", + }); + return proposals; } } - if (name === 'set_properties') { - if (typeof (a as any).path === 'string' && (a as any).props && typeof (a as any).props === 'object') { - const op: ObjectOp = { op: 'set_properties', path: (a as any).path, props: (a as any).props } - proposals.push({ id: id('obj'), type: 'object_op', ops: [op], notes: 'Parsed from set_properties' }) - return proposals + + if (name === "set_properties") { + if ( + typeof (a as any).path === "string" && + (a as any).props && + typeof (a as any).props === "object" + ) { + const op: ObjectOp = { + op: "set_properties", + path: (a as any).path, + props: (a as any).props, + }; + proposals.push({ + id: id("obj"), + type: "object_op", + ops: [op], + notes: "Parsed from set_properties", + }); + return proposals; } } - if (name === 'rename_instance') { - const path = ensurePath() - if (path && typeof (a as any).newName === 'string') { - proposals.push({ id: id('obj'), type: 'object_op', ops: [{ op: 'rename_instance', path, newName: (a as any).newName }], notes: 'Parsed from rename_instance' }) - return proposals + + if (name === "rename_instance") { + const path = ensurePath(); + if (path && typeof (a as any).newName === "string") { + proposals.push({ + id: id("obj"), + type: "object_op", + ops: [ + { + op: "rename_instance", + path, + newName: (a as any).newName, + }, + ], + notes: "Parsed from rename_instance", + }); + return proposals; } } - if (name === 'delete_instance') { - const path = ensurePath(selPath) + + if (name === "delete_instance") { + // const path = ensurePath(selPath) + const path = null; + console.log("selPath not found"); if (path) { // Guard: avoid destructive deletes at DataModel or Services level - if (/^game(\.[A-Za-z]+Service|\.DataModel)?$/.test(path)) return proposals - proposals.push({ id: id('obj'), type: 'object_op', ops: [{ op: 'delete_instance', path }], notes: 'Parsed from delete_instance' }) - return proposals + if (/^game(\.[A-Za-z]+Service|\.DataModel)?$/.test(path)) + return proposals; + proposals.push({ + id: id("obj"), + type: "object_op", + ops: [ + { + op: "delete_instance", + path, + }, + ], + notes: "Parsed from delete_instance", + }); + return proposals; } } - if (name === 'search_assets') { - const query = typeof (a as any).query === 'string' ? (a as any).query : (msg || 'button') - const tags = Array.isArray((a as any).tags) ? (a as any).tags.map(String) : undefined - const limit = typeof (a as any).limit === 'number' ? (a as any).limit : 6 - proposals.push({ id: id('asset'), type: 'asset_op', search: { query, tags, limit } }) - return proposals + + if (name === "search_assets") { + const query = + typeof (a as any).query === "string" ? (a as any).query : msg || "button"; + const tags = Array.isArray((a as any).tags) + ? (a as any).tags.map(String) + : undefined; + const limit = typeof (a as any).limit === "number" ? (a as any).limit : 6; + proposals.push({ + id: id("asset"), + type: "asset_op", + search: { query, tags, limit }, + }); + return proposals; } - if (name === 'insert_asset') { - const assetId = typeof (a as any).assetId === 'number' ? (a as any).assetId : Number((a as any).assetId) + + if (name === "insert_asset") { + const assetId = + typeof (a as any).assetId === "number" + ? (a as any).assetId + : Number((a as any).assetId); if (!isNaN(assetId)) { - proposals.push({ id: id('asset'), type: 'asset_op', insert: { assetId, parentPath: typeof (a as any).parentPath === 'string' ? (a as any).parentPath : undefined } }) - return proposals + proposals.push({ + id: id("asset"), + type: "asset_op", + insert: { + assetId, + parentPath: + typeof (a as any).parentPath === "string" + ? (a as any).parentPath + : undefined, + }, + }); + return proposals; } } - if (name === 'generate_asset_3d') { - if (typeof (a as any).prompt === 'string') { - proposals.push({ id: id('asset'), type: 'asset_op', generate3d: { prompt: (a as any).prompt, tags: Array.isArray((a as any).tags) ? (a as any).tags.map(String) : undefined, style: typeof (a as any).style === 'string' ? (a as any).style : undefined, budget: typeof (a as any).budget === 'number' ? (a as any).budget : undefined } }) - return proposals + + if (name === "generate_asset_3d") { + if (typeof (a as any).prompt === "string") { + proposals.push({ + id: id("asset"), + type: "asset_op", + generate3d: { + prompt: (a as any).prompt, + tags: Array.isArray((a as any).tags) + ? (a as any).tags.map(String) + : undefined, + style: + typeof (a as any).style === "string" ? (a as any).style : undefined, + budget: + typeof (a as any).budget === "number" + ? (a as any).budget + : undefined, + }, + }); + return proposals; } } - return proposals -} + return proposals; +} export async function runLLM(input: ChatInput): Promise { - const msg = input.message.trim() + const msg = input.message.trim(); - // Selection‑aware defaults - const selPath = input.context.selection && input.context.selection.length === 1 - ? input.context.selection[0].path - : undefined - const selIsContainer = !!selPath && /^(?:game\.(?:Workspace|ReplicatedStorage|ServerStorage|StarterGui|StarterPack|StarterPlayer|Lighting|Teams|SoundService|TextService|CollectionService)|game\.[A-Za-z]+\.[\s\S]+)/.test(selPath) + // Selection-aware defaults + const selPath = + input.context.selection && input.context.selection.length === 1 + ? input.context.selection[0].path + : undefined; + + const selIsContainer = + !!selPath && + /^(?:game\.(?:Workspace|ReplicatedStorage|ServerStorage|StarterGui|StarterPack|StarterPlayer|Lighting|Teams|SoundService|TextService|CollectionService)|game\.[A-Za-z]+\.[\s\S]+)/.test( + selPath, + ); // Provider gating - const providerRequested = !!(input.provider && input.provider.name === 'openrouter' && !!input.provider.apiKey) - const useProvider = providerRequested || process.env.VECTOR_USE_OPENROUTER === '1' - const streamKey = (input as any).workflowId || input.projectId - pushChunk(streamKey, `orchestrator.start provider=${useProvider ? 'openrouter' : 'fallback'} mode=${input.mode || 'agent'}`) - console.log(`[orch] start provider=${useProvider ? 'openrouter' : 'fallback'} mode=${input.mode || 'agent'} msgLen=${msg.length}`) + const providerRequested = !!( + input.provider && + input.provider.name === "openrouter" && + !!input.provider.apiKey + ); + const useProvider = + providerRequested || process.env.VECTOR_USE_OPENROUTER === "1"; + const streamKey = (input as any).workflowId || input.projectId; + + pushChunk( + streamKey, + `orchestrator.start provider=${useProvider ? "openrouter" : "fallback"} mode=${ + input.mode || "agent" + }`, + ); + + console.log( + `[orch] start provider=${useProvider ? "openrouter" : "fallback"} mode=${ + input.mode || "agent" + } msgLen=${msg.length}`, + ); // Deterministic templates for milestone verification - const lower = msg.toLowerCase() - const proposals: Proposal[] = [] - const addObj = (ops: ObjectOp[], notes?: string) => proposals.push({ id: id('obj'), type: 'object_op', ops, notes }) + const lower = msg.toLowerCase(); + const proposals: Proposal[] = []; + + const addObj = (ops: ObjectOp[], notes?: string) => + proposals.push({ id: id("obj"), type: "object_op", ops, notes }); + const makePartProps = (name: string, x: number, y: number, z: number) => ({ Name: name, Anchored: true, Size: { x: 4, y: 1, z: 4 }, CFrame: { x, y, z }, - }) + }); + + // === Grid 3x3 === if (/\b(grid\s*3\s*x\s*3|3\s*x\s*3\s*grid)\b/.test(lower)) { - const parent = 'game.Workspace' - addObj([{ op: 'create_instance', className: 'Model', parentPath: parent, props: { Name: 'Grid' } }], 'Create Grid model') - const basePath = 'game.Workspace.Grid' - const coords = [-4, 0, 4] + const parent = "game.Workspace"; + + addObj( + [ + { + op: "create_instance", + className: "Model", + parentPath: parent, + props: { Name: "Grid" }, + }, + ], + "Create Grid model", + ); + + const basePath = "game.Workspace.Grid"; + const coords = [-4, 0, 4]; + for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { - const name = `Cell_${i + 1}_${j + 1}` - const x = coords[j] - const z = coords[i] - addObj([ - { op: 'create_instance', className: 'Part', parentPath: basePath, props: makePartProps(name, x, 0.5, z) }, - ], `Create ${name}`) + const name = `Cell_${i + 1}_${j + 1}`; + const x = coords[j]; + const z = coords[i]; + addObj( + [ + { + op: "create_instance", + className: "Part", + parentPath: basePath, + props: makePartProps(name, x, 0.5, z), + }, + ], + `Create ${name}`, + ); } } - return proposals + return proposals; } + + // === Farming === if (/\bfarming\b/.test(lower)) { - const parent = 'game.Workspace' - addObj([{ op: 'create_instance', className: 'Model', parentPath: parent, props: { Name: 'Farm' } }], 'Create Farm model') - const basePath = 'game.Workspace.Farm' - addObj([{ op: 'create_instance', className: 'Part', parentPath: basePath, props: { Name: 'FarmBase', Anchored: true, Size: { x: 40, y: 1, z: 40 }, CFrame: { x: 0, y: 0.5, z: 0 } } }], 'Create Farm base') - // 4x4 soil grid = 16 steps including farm+base above → add 14 more - const coords = [-12, -4, 4, 12] - let count = 0 + const parent = "game.Workspace"; + + addObj( + [ + { + op: "create_instance", + className: "Model", + parentPath: parent, + props: { Name: "Farm" }, + }, + ], + "Create Farm model", + ); + + const basePath = "game.Workspace.Farm"; + + addObj( + [ + { + op: "create_instance", + className: "Part", + parentPath: basePath, + props: { + Name: "FarmBase", + Anchored: true, + Size: { x: 40, y: 1, z: 40 }, + CFrame: { x: 0, y: 0.5, z: 0 }, + }, + }, + ], + "Create Farm base", + ); + + // 4x4 soil grid = 16 steps including farm + base → add 14 more + const coords = [-12, -4, 4, 12]; + let count = 0; + for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { - if (count >= 14) break - const name = `Soil_${i + 1}_${j + 1}` - const x = coords[j] - const z = coords[i] - addObj([{ op: 'create_instance', className: 'Part', parentPath: basePath, props: makePartProps(name, x, 0.5, z) }], `Create ${name}`) - count++ + if (count >= 14) break; + + const name = `Soil_${i + 1}_${j + 1}`; + const x = coords[j]; + const z = coords[i]; + + addObj( + [ + { + op: "create_instance", + className: "Part", + parentPath: basePath, + props: makePartProps(name, x, 0.5, z), + }, + ], + `Create ${name}`, + ); + + count++; } - if (count >= 14) break + if (count >= 14) break; } - return proposals + return proposals; } - // Helper: build tool XML from name/args (for assistant history) + // === Helper: build tool XML for assistant history === const toXml = (name: string, args: Record): string => { - const parts: string[] = [`<${name}>`] + const parts: string[] = [`<${name}>`]; for (const [k, v] of Object.entries(args || {})) { - const val = typeof v === 'string' ? v : JSON.stringify(v) - parts.push(` <${k}>${val}`) + const val = typeof v === "string" ? v : JSON.stringify(v); + parts.push(` <${k}>${val}`); } - parts.push(``) - return parts.join('\n') - } + parts.push(``); + return parts.join("\n"); + }; - // Multi-turn Plan/Act loop + // === Multi-turn Plan/Act loop === if (useProvider) { - const defaultMaxTurns = Number(process.env.VECTOR_MAX_TURNS || 4) - const maxTurns = Number( - typeof input.maxTurns === 'number' + const defaultMaxTurns = Number(process.env.VECTOR_MAX_TURNS || 4); + const maxTurns = + typeof input.maxTurns === "number" ? input.maxTurns - : input.mode === 'ask' + : input.mode === "ask" ? 1 - : defaultMaxTurns, - ) - const messages: { role: 'user' | 'assistant'; content: string }[] = [{ role: 'user', content: msg }] - const validationRetryLimit = 2 - const unknownToolRetryLimit = 1 - let unknownToolRetries = 0 - let consecutiveValidationErrors = 0 + : defaultMaxTurns; + + const messages: { role: "user" | "assistant"; content: string }[] = [ + { role: "user", content: msg }, + ]; + + const validationRetryLimit = 2; + const unknownToolRetryLimit = 1; + let unknownToolRetries = 0; + let consecutiveValidationErrors = 0; for (let turn = 0; turn < maxTurns; turn++) { - let content = '' + let content = ""; + try { const resp = await callOpenRouter({ systemPrompt: SYSTEM_PROMPT, @@ -419,161 +691,253 @@ export async function runLLM(input: ChatInput): Promise { model: input.provider?.model, apiKey: input.provider?.apiKey, baseUrl: input.provider?.baseUrl, - }) - content = resp.content || '' - pushChunk(streamKey, `provider.response turn=${turn} chars=${content.length}`) - console.log(`[orch] provider.ok turn=${turn} contentLen=${content.length}`) + }); + + content = resp.content || ""; + pushChunk( + streamKey, + `provider.response turn=${turn} chars=${content.length}`, + ); + console.log( + `[orch] provider.ok turn=${turn} contentLen=${content.length}`, + ); } catch (e: any) { - pushChunk(streamKey, `error.provider ${e?.message || 'unknown'}`) - console.error(`[orch] provider.error ${e?.message || 'unknown'}`) - if (providerRequested) throw new Error(`Provider error: ${e?.message || 'unknown'}`) - break + pushChunk(streamKey, `error.provider ${e?.message || "unknown"}`); + console.error(`[orch] provider.error ${e?.message || "unknown"}`); + if (providerRequested) + throw new Error(`Provider error: ${e?.message || "unknown"}`); + break; } - const tool = parseToolXML(content) + const tool = parseToolXML(content); if (!tool) { - pushChunk(streamKey, 'error.validation no tool call parsed') - console.warn('[orch] parse.warn no tool call parsed') - if (providerRequested) throw new Error('Provider returned no parseable tool call') - break + pushChunk(streamKey, "error.validation no tool call parsed"); + console.warn("[orch] parse.warn no tool call parsed"); + if (providerRequested) + throw new Error("Provider returned no parseable tool call"); + break; } - const name = tool.name as keyof typeof Tools | string - let a: Record = tool.args || {} - pushChunk(streamKey, `tool.parsed ${String(name)}`) - console.log(`[orch] tool.parsed name=${String(name)}`) + const name = tool.name as keyof typeof Tools | string; + let a: Record = tool.args || {}; - // Infer missing fields from context (e.g., path) - if ((name === 'show_diff' || name === 'apply_edit') && !a.path && input.context.activeScript?.path) { - a = { ...a, path: input.context.activeScript.path } - } - if ((name === 'rename_instance' || name === 'set_properties' || name === 'delete_instance') && !a.path && selPath) { - a = { ...a, path: selPath } - } - if (name === 'create_instance' && !('parentPath' in a)) { - a = { ...a, parentPath: selIsContainer ? selPath! : 'game.Workspace' } - } - if (name === 'insert_asset' && !('parentPath' in a)) { - a = { ...a, parentPath: selIsContainer ? selPath! : 'game.Workspace' } - } + pushChunk(streamKey, `tool.parsed ${String(name)}`); + console.log(`[orch] tool.parsed name=${String(name)}`); + + // Infer missing fields from context + if ( + (name === "show_diff" || name === "apply_edit") && + !a.path && + input.context.activeScript?.path + ) { + a = { ...a, path: input.context.activeScript.path }; + } + if ( + (name === "rename_instance" || + name === "set_properties" || + name === "delete_instance") && + !a.path && + selPath + ) { + a = { ...a, path: selPath }; + } + if (name === "create_instance" && !("parentPath" in a)) { + a = { ...a, parentPath: selIsContainer ? selPath! : "game.Workspace" }; + } + if (name === "insert_asset" && !("parentPath" in a)) { + a = { ...a, parentPath: selIsContainer ? selPath! : "game.Workspace" }; + } - // Validate if known tool - const schema = (Tools as any)[name as any] as z.ZodTypeAny | undefined + // Validate tool schema + const schema = (Tools as any)[name as any] as z.ZodTypeAny | undefined; if (schema) { - const parsed = schema.safeParse(a) + const parsed = schema.safeParse(a); if (!parsed.success) { - consecutiveValidationErrors++ - const errMsg = parsed.error?.errors?.map((e) => `${e.path.join('.')}: ${e.message}`).join('; ') || 'invalid arguments' - pushChunk(streamKey, `error.validation ${String(name)} ${errMsg}`) - console.warn(`[orch] validation.error tool=${String(name)} ${errMsg}`) - // Reflect error verbatim and retry (up to limit) - messages.push({ role: 'assistant', content: toXml(String(name), a) }) - messages.push({ role: 'user', content: `VALIDATION_ERROR ${String(name)}\n${errMsg}` }) + consecutiveValidationErrors++; + const errMsg = + parsed.error?.errors + ?.map((e) => `${e.path.join(".")}: ${e.message}`) + .join("; ") || "invalid arguments"; + + pushChunk(streamKey, `error.validation ${String(name)} ${errMsg}`); + console.warn( + `[orch] validation.error tool=${String(name)} ${errMsg}`, + ); + + // Reflect error and retry + messages.push({ role: "assistant", content: toXml(String(name), a) }); + messages.push({ + role: "user", + content: `VALIDATION_ERROR ${String(name)}\n${errMsg}`, + }); + if (consecutiveValidationErrors > validationRetryLimit) { - throw new Error(`Validation failed repeatedly for ${String(name)}: ${errMsg}`) + throw new Error( + `Validation failed repeatedly for ${String(name)}: ${errMsg}`, + ); } - continue + continue; } else { - consecutiveValidationErrors = 0 - a = parsed.data - pushChunk(streamKey, `tool.valid ${String(name)}`) - console.log(`[orch] validation.ok tool=${String(name)}`) + consecutiveValidationErrors = 0; + a = parsed.data; + pushChunk(streamKey, `tool.valid ${String(name)}`); + console.log(`[orch] validation.ok tool=${String(name)}`); } } - // Context tools: execute locally, feed result back, and continue - const isContextTool = name === 'get_active_script' || name === 'list_selection' || name === 'list_open_documents' + // Context tools: execute locally + const isContextTool = + name === "get_active_script" || + name === "list_selection" || + name === "list_open_documents"; + if (isContextTool) { - const result = name === 'get_active_script' - ? (input.context.activeScript || null) - : name === 'list_selection' - ? (input.context.selection || []) - : (input.context.openDocs || []) - - // Avoid pushing extremely large buffers - const safeResult = name === 'get_active_script' && result && typeof (result as any).text === 'string' - ? { ...(result as any), text: ((result as any).text as string).slice(0, 40000) } - : result - - setLastTool(input.projectId, String(name), safeResult) - pushChunk(streamKey, `tool.result ${String(name)}`) - - // Append assistant tool call and user tool result to the conversation - messages.push({ role: 'assistant', content: toXml(String(name), a) }) - messages.push({ role: 'user', content: `TOOL_RESULT ${String(name)}\n` + JSON.stringify(safeResult) }) - continue + const result = + name === "get_active_script" + ? input.context.activeScript || null + : name === "list_selection" + ? input.context.selection || [] + : input.context.openDocs || []; + + const safeResult = + name === "get_active_script" && + result && + typeof (result as any).text === "string" + ? { + ...(result as any), + text: ((result as any).text as string).slice(0, 40000), + } + : result; + + setLastTool(input.projectId, String(name), safeResult); + pushChunk(streamKey, `tool.result ${String(name)}`); + + messages.push({ role: "assistant", content: toXml(String(name), a) }); + messages.push({ + role: "user", + content: `TOOL_RESULT ${String(name)}\n${JSON.stringify(safeResult)}`, + }); + + continue; } // Non-context tools → map to proposals and return - const mapped = mapToolToProposals(String(name), a, input, msg) + const mapped = mapToolToProposals(String(name), a, input, msg); if (mapped.length) { - pushChunk(streamKey, `proposals.mapped ${String(name)} count=${mapped.length}`) - console.log(`[orch] proposals.mapped tool=${String(name)} count=${mapped.length}`) - return mapped + pushChunk( + streamKey, + `proposals.mapped ${String(name)} count=${mapped.length}`, + ); + console.log( + `[orch] proposals.mapped tool=${String(name)} count=${mapped.length}`, + ); + return mapped; } - // Unknown tool: reflect error and allow a limited retry + // Unknown tool: reflect error and retry if (!(Tools as any)[name as any]) { - unknownToolRetries++ - const errMsg = `Unknown tool: ${String(name)}` - pushChunk(streamKey, `error.validation ${errMsg}`) - console.warn(`[orch] unknown.tool ${String(name)}`) - messages.push({ role: 'assistant', content: toXml(String(name), a) }) - messages.push({ role: 'user', content: `VALIDATION_ERROR ${String(name)}\n${errMsg}` }) - if (unknownToolRetries > unknownToolRetryLimit) break - continue + unknownToolRetries++; + const errMsg = `Unknown tool: ${String(name)}`; + pushChunk(streamKey, `error.validation ${errMsg}`); + console.warn(`[orch] unknown.tool ${String(name)}`); + + messages.push({ role: "assistant", content: toXml(String(name), a) }); + messages.push({ + role: "user", + content: `VALIDATION_ERROR ${String(name)}\n${errMsg}`, + }); + + if (unknownToolRetries > unknownToolRetryLimit) break; + continue; } - // Otherwise break to fallbacks - break + break; // fallbacks } } - // Fallbacks: safe, deterministic proposals without provider parsing - const fallbacksEnabled = typeof (input as any).enableFallbacks === 'boolean' - ? (input as any).enableFallbacks - : (process.env.VECTOR_DISABLE_FALLBACKS || '1') !== '1' - const fallbacksDisabled = !fallbacksEnabled + // === Fallbacks === + const fallbacksEnabled = + typeof (input as any).enableFallbacks === "boolean" + ? (input as any).enableFallbacks + : (process.env.VECTOR_DISABLE_FALLBACKS || "1") !== "1"; + + const fallbacksDisabled = !fallbacksEnabled; + if (!fallbacksDisabled && input.context.activeScript) { - const path = input.context.activeScript.path - const prefixComment = `-- Vector: ${sanitizeComment(msg)}\n` - const edits = [{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 }, text: prefixComment }] - const old = input.context.activeScript.text - const next = applyRangeEdits(old, edits) - const unified = simpleUnifiedDiff(old, next, path) - pushChunk(streamKey, 'fallback.edit commentTop') - console.log('[orch] fallback.edit inserting comment at top') - return [{ - id: id('edit'), - type: 'edit', - path, - notes: 'Insert a comment at the top as a placeholder for an edit.', - diff: { mode: 'rangeEDITS', edits }, - preview: { unified }, - safety: { beforeHash: (require('node:crypto') as typeof import('node:crypto')).createHash('sha1').update(old).digest('hex') }, - } as any] + const path = input.context.activeScript.path; + const prefixComment = `-- Vector: ${sanitizeComment(msg)}\n`; + + const edits = [ + { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + text: prefixComment, + }, + ]; + + const old = input.context.activeScript.text; + const next = applyRangeEdits(old, edits); + const unified = simpleUnifiedDiff(old, next, path); + + pushChunk(streamKey, "fallback.edit commentTop"); + console.log("[orch] fallback.edit inserting comment at top"); + + return [ + { + id: id("edit"), + type: "edit", + path, + notes: "Insert a comment at the top as a placeholder for an edit.", + diff: { mode: "rangeEDITS", edits }, + preview: { unified }, + safety: { + beforeHash: (require("node:crypto") as typeof import("node:crypto")) + .createHash("sha1") + .update(old) + .digest("hex"), + }, + } as any, + ]; } - if (!fallbacksDisabled && input.context.selection && input.context.selection.length > 0) { - const first = input.context.selection[0] - pushChunk(streamKey, `fallback.object rename ${first.path}`) - console.log(`[orch] fallback.object rename path=${first.path}`) + if ( + !fallbacksDisabled && + input.context.selection && + input.context.selection.length > 0 + ) { + const first = input.context.selection[0]; + + pushChunk(streamKey, `fallback.object rename ${first.path}`); + console.log(`[orch] fallback.object rename path=${first.path}`); + return [ { - id: id('obj'), - type: 'object_op', - notes: 'Rename selected instance by appending _Warp', - ops: [{ op: 'rename_instance', path: first.path, newName: `${first.path.split('.').pop() || 'Instance'}_Warp` }], + id: id("obj"), + type: "object_op", + notes: "Rename selected instance by appending Vector", + ops: [ + { + op: "rename_instance", + path: first.path, + newName: `${first.path.split(".").pop() || "Instance"}_Vector`, + }, + ], }, - ] + ]; } if (!fallbacksDisabled) { - pushChunk(streamKey, 'fallback.asset search') - console.log('[orch] fallback.asset search') + pushChunk(streamKey, "fallback.asset search"); + console.log("[orch] fallback.asset search"); + return [ - { id: id('asset'), type: 'asset_op', search: { query: msg || 'button', limit: 6 } }, - ] + { + id: id("asset"), + type: "asset_op", + search: { query: msg || "button", limit: 6 }, + }, + ]; } - throw new Error('No actionable tool produced within turn limit') + + throw new Error("No actionable tool produced within turn limit"); }