import { createStudioRender } from '../../domain/factories' import type { StudioFileAttachment, StudioToolDefinition, StudioToolResult } from '../../domain/types' import type { StudioRuntimeBackedToolContext } from '../../runtime/tools/tool-runtime-context' import { isStudioRunCancelledError } from '../../runtime/execution/run-cancellation' import { createUnconfiguredPlotRenderPort, type PlotRenderExecution, type PlotRenderPort } from '../plot-render-port' import { plotRenderToolParameters } from '../../tools/tool-parameters' import { publishStudioRenderUpdated } from '../../render/render-events' import { toWorkspaceRelativePath } from '../../tools/workspace-paths' interface PlotRenderToolInput { concept: string code: string } export function createPlotStudioRenderTool( renderPort: PlotRenderPort = createUnconfiguredPlotRenderPort() ): StudioToolDefinition { return { name: 'render', parameters: plotRenderToolParameters, description: 'Execute matplotlib code and persist static plot outputs for preview.', allowedAgents: ['builder'], allowedStudioKinds: ['plot'], execute: async (input, context) => executePlotRenderTool(input, context as StudioRuntimeBackedToolContext, renderPort) } } async function executePlotRenderTool( input: PlotRenderToolInput, context: StudioRuntimeBackedToolContext, renderPort: PlotRenderPort ): Promise { if (!input.concept?.trim() || !input.code?.trim()) { throw new Error('Render tool requires non-empty "concept" and "code"') } if (!context.renderStore) { throw new Error('Render store is unavailable') } const title = `Plot render: ${input.concept.slice(0, 80)}` const render = createStudioRender({ ownerId: context.session.ownerId, sessionId: context.session.id, runId: context.run.id, kind: 'plot', title, concept: input.concept, outputMode: 'image', quality: 'medium', status: 'running', metadata: { studioKind: 'plot', outputMode: 'image' }, }) render.metadata = { ...(render.metadata ?? {}), renderId: render.id } const createdRender = await context.renderStore.create(render) publishStudioRenderUpdated(context.eventBus, createdRender) context.setToolMetadata?.({ title, metadata: { renderId: render.id, studioKind: 'plot' } }) try { const execution = await renderPort.execute({ workspaceDirectory: context.session.directory, renderId: render.id, code: input.code, signal: context.abortSignal, }) const attachments = buildAttachments(execution.imageDataUris, execution.imagePaths) const completed = await context.renderStore.update(context.session.ownerId, render.id, { status: 'completed', attachments, metadata: { ...(render.metadata ?? {}), imageCount: execution.imageDataUris.length, scriptPath: toWorkspaceRelativePath(context.session.directory, execution.scriptPath).replace(/\\/g, '/'), imagePaths: execution.imagePaths.map((imagePath) => toWorkspaceRelativePath(context.session.directory, imagePath).replace(/\\/g, '/')), }, }) if (!completed) { throw new Error('Render record disappeared before completion') } publishStudioRenderUpdated(context.eventBus, completed) return { title, output: `plot_render_id: ${render.id}`, attachments, metadata: { renderId: render.id, imageCount: execution.imageDataUris.length, scriptPath: completed.metadata?.scriptPath, imagePaths: completed.metadata?.imagePaths, }, } } catch (error) { const failedRender = await context.renderStore.update(context.session.ownerId, render.id, { status: isStudioRunCancelledError(error) ? 'cancelled' : 'failed', error: error instanceof Error ? error.message : String(error), }) if (failedRender) { publishStudioRenderUpdated(context.eventBus, failedRender) } throw error } } function buildAttachments(imageDataUris: string[], imagePaths: string[]): StudioFileAttachment[] { return imageDataUris.map((path, index) => ({ kind: 'file', path, name: fileNameFromPath(imagePaths[index]) || `plot_${index + 1}.png`, mimeType: 'image/png' })) } function fileNameFromPath(path?: string): string { if (!path) { return '' } const parts = path.split(/[\\/]/) return parts[parts.length - 1] || path }