From 2c3d3f1debe2402cf5b294fa2e22b9ce89af3935 Mon Sep 17 00:00:00 2001 From: Ariel Weinberger Date: Mon, 21 Jul 2025 10:28:25 -0500 Subject: [PATCH] fixes --- ai-service/docker-compose.yml | 4 +- ai-service/src/handlers/chat.handler.ts | 328 ++++++++++-------- ai-service/src/handlers/moonshotai.ts | 0 .../src/lib/ai/mastra/tools/file-tools.ts | 151 ++++---- ai-service/src/lib/daytona/Dockerfile-daytona | 4 - ai-service/src/lib/daytona/daytona-client.ts | 2 +- ai-service/src/lib/daytona/daytona-sandbox.ts | 63 +++- ai-service/src/lib/synapse-http-client.ts | 24 -- src/lib/components/studio/chat/chat.svelte | 4 +- 9 files changed, 313 insertions(+), 267 deletions(-) create mode 100644 ai-service/src/handlers/moonshotai.ts diff --git a/ai-service/docker-compose.yml b/ai-service/docker-compose.yml index 99d7ade2b..f4eb95da0 100644 --- a/ai-service/docker-compose.yml +++ b/ai-service/docker-compose.yml @@ -10,6 +10,4 @@ services: required: true environment: SYNAPSE_ENDPOINT: http://host.docker.internal:3001 - APPWRITE_ENDPOINT: http://host.docker.internal:80/v1 - volumes: - - ./tmp:/app/tmp \ No newline at end of file + APPWRITE_ENDPOINT: http://host.docker.internal:80/v1 \ No newline at end of file diff --git a/ai-service/src/handlers/chat.handler.ts b/ai-service/src/handlers/chat.handler.ts index 0b9dbaca5..bbfab3fdc 100644 --- a/ai-service/src/handlers/chat.handler.ts +++ b/ai-service/src/handlers/chat.handler.ts @@ -9,7 +9,6 @@ import { generateId, hasToolCall, smoothStream, - streamObject, streamText, TextPart, tool @@ -20,9 +19,7 @@ import { anthropic } from '@ai-sdk/anthropic'; import { getOrCreateArtifactSandbox, startDevServer } from '@/lib/daytona/daytona-sandbox'; import { OnStepUpdateFn, workspaceStepSchema } from '@/lib/ai/custom-parts/workspace-state'; import { getOrCreateConversation, updateConversationHistory } from './message-history'; -import { imagineToMastraToolset } from '@/lib/ai/mastra/tools/imagine-tool'; import { fileTools } from '@/lib/ai/mastra/tools/file-tools'; -import { createSystemPrompt } from '@/lib/ai/system-prompt/create-system-prompt'; import { GitRepositoryUtils } from '@/lib/git-utils'; import { getPagesAndComponents, @@ -73,10 +70,14 @@ export const handleChatRequest = async (c: Context) => { if (trigger === 'submit-tool-result') { // save to file // return c.body(null, 200); - return streamText({ - model: anthropic('claude-3-7-sonnet-20250219'), - messages: convertToModelMessages(messages) - }).toUIMessageStreamResponse(); + // return streamText({ + // model: anthropic('claude-3-7-sonnet-20250219'), + // messages: convertToModelMessages(messages) + // }).toUIMessageStreamResponse(); + // return c.json({ + // error: 'Not implemented' + // }, 500); + console.log('submit-tool-result'); } const { imagineClient, workspacesClient } = await createImagineClient({ @@ -110,8 +111,8 @@ export const handleChatRequest = async (c: Context) => { writer.write({ type: 'start', - messageId, - }) + messageId + }); const updateStep: OnStepUpdateFn = ({ id, @@ -130,6 +131,7 @@ export const handleChatRequest = async (c: Context) => { if (steps.length > 0) { writer.write({ type: 'data-workspace-state', + transient: true, data: { state: 'in-progress', steps, @@ -172,7 +174,6 @@ export const handleChatRequest = async (c: Context) => { } }); - /** SYSTEM PROMPT */ const gitRepositoryUtils = new GitRepositoryUtils( @@ -201,147 +202,178 @@ export const handleChatRequest = async (c: Context) => { /** */ - const thinkingStartTime = Date.now(); - const thinkingId = createIdGenerator({ size: 10 })(); - let thoughts = ''; + // const thinkingStartTime = Date.now(); + // const thinkingId = createIdGenerator({ size: 10 })(); + // let thoughts = ''; - const planResult = streamText({ - maxRetries: 3, - model: anthropic('claude-sonnet-4-20250514'), - abortSignal: signal, - tools: { - readFile: fileTools.readFileTool, - listFilesInDirectory: fileTools.listFilesInDirectoryTool, - readMultipleFilesInParallel: fileTools.readMultipleFilesInParallelTool, - reportPlan: tool({ - name: 'reportPlan', - description: 'You use this tool to report your final plan for implementation.', - inputSchema: z.object({ - plan: z.string().describe('A detailed plan for implementation. Must be a few paragraphs long.'), - }), - execute: async ({ plan }) => { - console.log('reportPlan', plan); - return 'Your plan has been reported.'; - } - }) - }, - stopWhen: hasToolCall('reportPlan'), - activeTools: ["readFile", "listFilesInDirectory", "readMultipleFilesInParallel", "reportPlan"], - messages: [ - { - role: 'system', - content: dedent(` - You are an elite software architect. + // console.log('[planResult] start'); - Here is the current file layout and dependencies in the project: - ${currentCodeContext.content} + // const planResult = streamText({ + // maxRetries: 3, + // model: anthropic('claude-sonnet-4-20250514'), + // abortSignal: signal, + // tools: { + // readFile: fileTools.readFileTool, + // listFilesInDirectory: fileTools.listFilesInDirectoryTool, + // readMultipleFilesInParallel: fileTools.readMultipleFilesInParallelTool, + // reportPlan: tool({ + // name: 'reportPlan', + // description: + // 'You use this tool to report your final plan for implementation.', + // inputSchema: z.object({ + // plan: z + // .string() + // .describe( + // 'A detailed plan for implementation. Must be a few paragraphs long.' + // ) + // }), + // execute: async ({ plan }) => { + // console.log('reportPlan', plan); + // return 'Your plan has been reported.'; + // } + // }) + // }, + // stopWhen: hasToolCall('reportPlan'), + // activeTools: [ + // 'readFile', + // 'listFilesInDirectory', + // 'readMultipleFilesInParallel', + // 'reportPlan' + // ], + // messages: [ + // { + // role: 'system', + // content: dedent(` + // You are an elite software architect. + + // Here is the current file layout and dependencies in the project: + // ${currentCodeContext.content} - Please plan the steps to implement the user's request. You do not write any code. - You may read files to understand the contents of files, which will help you make a better plan. - Your plans should be concise and to the point. + // Please plan the steps to implement the user's request. You do not write any code. + // You may read files to understand the contents of files, which will help you make a better plan. + // Your plans should be concise and to the point. - You will produce the following outputs: - - A short and concise plan - - Whether a UI developer is needed + // You will produce the following outputs: + // - A short and concise plan + // - Whether a UI developer is needed - # Outputs + // # Outputs - ## Plan - A short and concise plan of what exactly needs to be done to achieve the product manager's request. This should be 3-4 sentences maximum and in first person. E.g. 'I will ... and then ... ' + // ## Plan + // A short and concise plan of what exactly needs to be done to achieve the product manager's request. This should be 3-4 sentences maximum and in first person. E.g. 'I will ... and then ... ' - ## When is a UI developer needed? - This is important because the UI developer will be the one to implement the design. + // ## When is a UI developer needed? + // This is important because the UI developer will be the one to implement the design. - Examples of when a UI developer IS needed: - - Create new screens - - Modify existing screens with explicit design requests (e.g. adding elements or restructuring the layout) + // Examples of when a UI developer IS needed: + // - Create new screens + // - Modify existing screens with explicit design requests (e.g. adding elements or restructuring the layout) - Examples of when a UI developer IS NOT needed: - - Adding a React hook - - Adding an endpoint - - Pure logic changes - - Moving files around, renaming files, etc. + // Examples of when a UI developer IS NOT needed: + // - Adding a React hook + // - Adding an endpoint + // - Pure logic changes + // - Moving files around, renaming files, etc. - In termso f filesystem tool usage: - - You ONLY have access to read-only filesystem tools - `) - }, - ...restMessages.map((m: any) => ({ - role: m.role, - content: m.content - })), - { - role: 'user', - content: ` + // In termso f filesystem tool usage: + // - You ONLY have access to read-only filesystem tools + // `) + // }, + // ...restMessages.map((m: any) => ({ + // role: m.role, + // content: m.content + // })), + // { + // role: 'user', + // content: ` - Here is the product manager's request: - - ${latestMessageTextPart.text} - - ` - } - ], - headers: { - 'anthropic-beta': [ - 'token-efficient-tools-2025-02-19', - 'fine-grained-tool-streaming-2025-05-14', - 'extended-cache-ttl-2025-04-11' - ].join(',') - }, - experimental_transform: smoothStream({ - delayInMs: 30, - chunking: 'word' - }), - onChunk: (event) => { - if (event.chunk.type === 'text') { - thoughts += event.chunk.text; + // Here is the product manager's request: + // + // ${latestMessageTextPart.text} + // + // ` + // } + // ], + // headers: { + // 'anthropic-beta': [ + // 'token-efficient-tools-2025-02-19', + // 'fine-grained-tool-streaming-2025-05-14', + // 'extended-cache-ttl-2025-04-11' + // ].join(',') + // }, + // experimental_transform: smoothStream({ + // delayInMs: 30, + // chunking: 'word' + // }), + // onChunk: (event) => { + // if (event.chunk.type === 'text') { + // thoughts += event.chunk.text; - writer.write({ - type: 'data-thinking', - id: thinkingId, - data: { - text: thoughts, - durationMs: null, - state: 'streaming' + // writer.write({ + // type: 'data-thinking', + // id: thinkingId, + // data: { + // text: thoughts, + // durationMs: null, + // state: 'streaming' + // } + // }); + // } + // }, + // onFinish: () => { + // console.log('[planResult] onFinish', { thoughts }); + // const thinkingEndTime = Date.now(); + // const thinkingDuration = thinkingEndTime - thinkingStartTime; + // writer.write({ + // type: 'data-thinking', + // id: thinkingId, + // data: { + // text: thoughts, + // durationMs: thinkingDuration, + // state: 'done' + // } + // }); + // } + // }); + + // for await (const chunk of planResult.textStream) { + // } + + // const planToolCall = (await planResult.toolCalls).find( + // (call) => call.toolName === 'reportPlan' + // ); + + // // if (!planToolCall) { + // // writer.write({ + // // type: "finish", + // // }) + // // return; + // // } + + // const plan = planToolCall?.input?.plan ?? ''; + + // console.log('[plan]', plan); + + console.log('[implementResult] start'); + + /* + + ${ + plan + ? dedent(` + Here is the plan from the architect on how to implement it: + + ${plan} + `) + : '' } - }); - } - }, - onFinish: () => { - const thinkingEndTime = Date.now(); - const thinkingDuration = thinkingEndTime - thinkingStartTime; - writer.write({ - type: 'data-thinking', - id: thinkingId, - data: { - text: thoughts, - durationMs: thinkingDuration, - state: 'done' - } - }); - } - }); - - for await (const chunk of planResult.textStream) { } - - const planToolCall = (await planResult.toolCalls).find((call) => call.toolName === "reportPlan"); - - if (!planToolCall) { - writer.write({ - type: "finish", - }) - return; - } - - const plan = planToolCall.input.plan; - - console.log('plan', plan); + */ const implementResult = streamText({ maxOutputTokens: 52000, maxRetries: 5, - model: anthropic('claude-sonnet-4-20250514'), + model: anthropic('claude-3-7-sonnet-20250219'), stopWhen: hasToolCall('reportDone'), tools: { readFile: fileTools.readFileTool, @@ -385,11 +417,6 @@ export const handleChatRequest = async (c: Context) => { ${latestMessageTextPart.text} - - Here is the plan from the architect on how to implement it: - - ${plan} - ` } ], @@ -406,7 +433,7 @@ export const handleChatRequest = async (c: Context) => { }), onStepFinish: async (event) => { console.log('[implementResult onStepFinish] onStepFinish'); - + const changedFiles = runtimeContext.get('changedFiles'); console.log('[implementResult onStepFinish] changedFiles', changedFiles); @@ -419,20 +446,23 @@ export const handleChatRequest = async (c: Context) => { return { path: f.path, content: content.content - } + }; }) - ) + ); - await updateDependencies( - filesWithContents - ) + await updateDependencies(filesWithContents); + }, + onFinish: () => { + console.log('[implementResult] onFinish'); } }); - writer.merge(implementResult.toUIMessageStream({ - originalMessages: messages, - sendStart: false, - })); + writer.merge( + implementResult.toUIMessageStream({ + originalMessages: messages, + sendStart: false + }) + ); }, onFinish: async (event) => { console.log('onFinish', { didError }); @@ -466,6 +496,6 @@ export const handleChatRequest = async (c: Context) => { }); return createUIMessageStreamResponse({ - stream, + stream }); }; diff --git a/ai-service/src/handlers/moonshotai.ts b/ai-service/src/handlers/moonshotai.ts new file mode 100644 index 000000000..e69de29bb diff --git a/ai-service/src/lib/ai/mastra/tools/file-tools.ts b/ai-service/src/lib/ai/mastra/tools/file-tools.ts index 548dd30e0..a9a371f60 100644 --- a/ai-service/src/lib/ai/mastra/tools/file-tools.ts +++ b/ai-service/src/lib/ai/mastra/tools/file-tools.ts @@ -25,9 +25,9 @@ const readMultipleFilesInParallelTool = tool({ const runtimeContext = getContext().var.runtimeContext; const writer = getWriterFromContext(runtimeContext); - writer.write({ - type: 'start-step' - }); + // writer.write({ + // type: 'start-step' + // }); console.log(`[TOOL - readMultipleFilesInParallel]`, { paths }); @@ -42,9 +42,9 @@ const readMultipleFilesInParallelTool = tool({ }) ); - writer.write({ - type: 'finish-step' - }); + // writer.write({ + // type: 'finish-step' + // }); return { files }; } @@ -74,9 +74,9 @@ const readFileTool = tool({ const writer = getWriterFromContext(runtimeContext); const toolCallId = createIdGenerator({ size: 10 })(); - writer.write({ - type: 'start-step' - }); + // writer.write({ + // type: 'start-step' + // }); if (!skipWritingToolCalls) { writer.write({ @@ -92,20 +92,20 @@ const readFileTool = tool({ const result = await readFileFn(`./${path}`); console.log(`[TOOL - readFile] Reading file at path: ${path}`); - if (!skipWritingToolCalls) { - writer.write({ - type: 'tool-output-available', - toolCallId, - output: { - path: result.path, - content: result.content - } - }); - } + // if (!skipWritingToolCalls) { + // writer.write({ + // type: 'tool-output-available', + // toolCallId, + // output: { + // path: result.path, + // content: result.content + // } + // }); + // } - writer.write({ - type: 'finish-step' - }); + // writer.write({ + // type: 'finish-step' + // }); return { path: result.path, @@ -140,15 +140,15 @@ const writeFilesTool = tool({ const writer = getWriterFromContext(runtimeContext); const synapse = runtimeContext.get('synapseClient'); - writer.write({ - type: 'start-step' - }); + // writer.write({ + // type: 'start-step' + // }); const { successFiles, errorFiles } = await writeFiles(files, synapse); - writer.write({ - type: 'finish-step' - }); + // writer.write({ + // type: 'finish-step' + // }); return { successFiles, @@ -169,7 +169,7 @@ const writeFileTool = tool({ success: z.boolean().describe('Whether the file was written successfully'), error: z .string() - .optional() + .nullable() .describe('The error message if the file was not written successfully') }), execute: async ({ path, content }) => { @@ -178,23 +178,23 @@ const writeFileTool = tool({ const writer = getWriterFromContext(runtimeContext); const synapse = runtimeContext.get('synapseClient'); - writer.write({ - type: 'start-step' - }); + // writer.write({ + // type: 'start-step' + // }); - const toolCallId = createIdGenerator({ size: 10 })(); + // const toolCallId = createIdGenerator({ size: 10 })(); - if (!skipWritingToolCalls) { - writer.write({ - type: 'tool-input-available', - toolCallId, - toolName: 'writeFile', - input: { - path, - content - } - }); - } + // if (!skipWritingToolCalls) { + // writer.write({ + // type: 'tool-input-available', + // toolCallId, + // toolName: 'writeFile', + // input: { + // path, + // content + // } + // }); + // } const { errorFiles, successFiles } = await writeFiles([{ path, content }], synapse); @@ -204,20 +204,21 @@ const writeFileTool = tool({ console.log('changedFiles', newChangedFiles); - if (!skipWritingToolCalls) { - writer.write({ - type: 'tool-output-available', - toolCallId, - output: { - success: true - } - }); - } - writer.write({ - type: 'finish-step' - }); + // if (!skipWritingToolCalls) { + // writer.write({ + // type: 'tool-output-available', + // toolCallId, + // output: { + // success: true + // } + // }); + // } + // writer.write({ + // type: 'finish-step' + // }); return { - success: true + success: true, + error: null }; } }); @@ -270,9 +271,9 @@ const listFilesInDirectoryTool = tool({ const writer = getWriterFromContext(runtimeContext); const synapse = runtimeContext.get('synapseClient'); - writer.write({ - type: 'start-step' - }); + // writer.write({ + // type: 'start-step' + // }); console.log( `[TOOL - listFilesInDirectory] Listing files in directory at path: ${path}, recursive: ${recursive}` @@ -288,9 +289,9 @@ const listFilesInDirectoryTool = tool({ `[TOOL - listFilesInDirectory] Found ${files.length} files in directory at path: ${path}` ); - writer.write({ - type: 'finish-step' - }); + // writer.write({ + // type: 'finish-step' + // }); return { files @@ -313,15 +314,15 @@ const deleteFileTool = tool({ const writer = getWriterFromContext(runtimeContext); const synapse = runtimeContext.get('synapseClient'); - writer.write({ - type: 'start-step' - }); + // writer.write({ + // type: 'start-step' + // }); await synapse.deleteFile({ filepath: `./${path}` }); - writer.write({ - type: 'finish-step' - }); + // writer.write({ + // type: 'finish-step' + // }); return { success: true @@ -351,15 +352,15 @@ const moveFileTool = tool({ const writer = getWriterFromContext(runtimeContext); const synapse = runtimeContext.get('synapseClient'); - writer.write({ - type: 'start-step' - }); + // writer.write({ + // type: 'start-step' + // }); await synapse.updateFilePath({ filepath: `./${path}`, newPath: `./${newPath}` }); - writer.write({ - type: 'finish-step' - }); + // writer.write({ + // type: 'finish-step' + // }); return { success: true diff --git a/ai-service/src/lib/daytona/Dockerfile-daytona b/ai-service/src/lib/daytona/Dockerfile-daytona index c4eea58ce..46d3a2acb 100644 --- a/ai-service/src/lib/daytona/Dockerfile-daytona +++ b/ai-service/src/lib/daytona/Dockerfile-daytona @@ -8,10 +8,6 @@ RUN npm -v RUN npm install -g bun RUN bunx giget --version -RUN ls -la -RUN bunx giget@latest gh:appwrite/templates-for-frameworks/base-vite-template . -RUN bun install - EXPOSE 3000 ENTRYPOINT ["sleep", "infinity"] \ No newline at end of file diff --git a/ai-service/src/lib/daytona/daytona-client.ts b/ai-service/src/lib/daytona/daytona-client.ts index 8029b87b9..9dc5189de 100644 --- a/ai-service/src/lib/daytona/daytona-client.ts +++ b/ai-service/src/lib/daytona/daytona-client.ts @@ -3,6 +3,6 @@ import { Daytona } from "@daytonaio/sdk"; export const daytona = new Daytona({ target: "us", - apiKey: "dtn_4ceb903baf0ef4264e457d2401d904b01c63e11a487d737be0a6113ebeb62ff0", + apiKey: process.env.DAYTONA_API_KEY!, apiUrl: "https://app.daytona.io/api" }); diff --git a/ai-service/src/lib/daytona/daytona-sandbox.ts b/ai-service/src/lib/daytona/daytona-sandbox.ts index ca1acb461..c1fcf20e7 100644 --- a/ai-service/src/lib/daytona/daytona-sandbox.ts +++ b/ai-service/src/lib/daytona/daytona-sandbox.ts @@ -25,10 +25,10 @@ const createSandbox = async ({ artifactId }: { artifactId: string }) => { let sandbox: Sandbox; try { sandbox = await daytona.create({ - snapshot: 'imgn-base-vite:v2', + snapshot: 'imgn-base-vite:v4', labels: { artifactId }, autoStopInterval: 10, // 10 minutes - autoDeleteInterval: 60, // 20 minutes + autoDeleteInterval: 60, // 60 minutes // language: 'typescript', public: true }); @@ -109,6 +109,13 @@ export const getOrCreateArtifactSandbox = async ({ console.log('Sandbox started'); } + onStepUpdate({ + id: WorkspaceStepId.CREATE_SANDBOX, + status: 'completed', + text: 'Workspace set up' + }); + + sandbox = foundSandbox; } else { console.log('Workspace not found, creating...'); @@ -123,6 +130,52 @@ export const getOrCreateArtifactSandbox = async ({ artifactId }); + onStepUpdate({ + id: WorkspaceStepId.CREATE_SANDBOX, + status: 'completed', + text: 'Workspace created' + }); + + + onStepUpdate({ + id: WorkspaceStepId.REPOSITORY_SETUP, + status: 'in-progress', + text: 'Setting up repo...' + }); + + await sandbox.process.executeCommand( + 'bunx giget@latest gh:appwrite/templates-for-frameworks/base-vite-template .', + '/home/daytona/workspace', + {}, + 30 + ); + + onStepUpdate({ + id: WorkspaceStepId.REPOSITORY_SETUP, + status: 'completed', + text: 'Repository set up' + }); + + onStepUpdate({ + id: WorkspaceStepId.INSTALL_DEPENDENCIES, + status: 'in-progress', + text: 'Installing dependencies...' + }); + + await sandbox.process.executeCommand( + 'bun install', + '/home/daytona/workspace', + {}, + 30 + ); + + onStepUpdate({ + id: WorkspaceStepId.INSTALL_DEPENDENCIES, + status: 'completed', + text: 'Dependencies installed' + }); + + // Save sandbox id to db await prisma.conversation.update({ where: { @@ -137,12 +190,6 @@ export const getOrCreateArtifactSandbox = async ({ console.log('Created sandbox', sandbox); } - onStepUpdate({ - id: WorkspaceStepId.CREATE_SANDBOX, - status: 'completed', - text: 'Workspace found' - }); - return { sandbox }; }; diff --git a/ai-service/src/lib/synapse-http-client.ts b/ai-service/src/lib/synapse-http-client.ts index f5bd1d288..578005c03 100644 --- a/ai-service/src/lib/synapse-http-client.ts +++ b/ai-service/src/lib/synapse-http-client.ts @@ -17,7 +17,6 @@ export type ListFilesInDirParams = { export class SynapseHTTPClient { private sandbox: Sandbox; private endpoint: string; - private artifactBasePath: string; private artifactId: string; constructor({ @@ -30,7 +29,6 @@ export class SynapseHTTPClient { sandbox: Sandbox; }) { this.endpoint = endpoint; - this.artifactBasePath = `/usr/local/artifact`; this.artifactId = artifactId; this.sandbox = sandbox; } @@ -48,28 +46,6 @@ export class SynapseHTTPClient { }; } - async createFile({ - path, - content, - ignoreBasePath = false - }: { - path: string; - content: string; - ignoreBasePath?: boolean; - }) { - console.log('createFile', { path, content }); - const response = await this.request({ - type: 'fs', - operation: 'createFile', - params: { - filepath: ignoreBasePath ? path : _path.resolve(this.artifactBasePath, path), - content - } - }); - - return response; - } - async createOrUpdateFile({ filepath, content }: { filepath: string; content: string }) { return daytonaFs.createOrUpdateFile({ sandbox: this.sandbox, diff --git a/src/lib/components/studio/chat/chat.svelte b/src/lib/components/studio/chat/chat.svelte index 336d15423..b26be2f1e 100644 --- a/src/lib/components/studio/chat/chat.svelte +++ b/src/lib/components/studio/chat/chat.svelte @@ -34,15 +34,13 @@ const chat = new Chat({ - maxSteps: 20, + maxSteps: 50, id: $conversation.data.id, transport: new DefaultChatTransport({ api: `${VARS.AI_SERVICE_BASE_URL}/api/chat`, }), messages: $conversation.data.messages ?? [], onData: (dataPart) => { - console.log("data", dataPart); - if (dataPart.type === "data-workspace-state") { const data = dataPart.data as ImagineUIDataParts["workspace-state"]; const { state, workspaceUrl, steps } = data;