File size: 4,116 Bytes
6dc0fd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import { createStudioRender } from '../../../domain/factories'
import type { StudioToolDefinition, StudioToolResult } from '../../../domain/types'
import type { StudioRuntimeBackedToolContext } from '../../../runtime/tools/tool-runtime-context'
import { throwIfStudioRunCancelled } from '../../../runtime/execution/run-cancellation'
import type { StudioRenderDriver } from '../../../render/studio-render-driver'
import { plotRenderToolParameters } from '../../../tools/tool-parameters'
import { publishStudioRenderUpdated } from '../../../render/render-events'
import {
  executeStudioRenderLifecycle,
  studioRenderFailureMessage,
  studioRenderFailureStatus
} from '../../../render/render-lifecycle'

interface PlotRenderToolInput {
  concept: string
  code: string
}

export function createPlotStudioRenderTool(
  driver: StudioRenderDriver
): StudioToolDefinition<PlotRenderToolInput> {
  return {
    name: 'render',
    parameters: plotRenderToolParameters,
    description: 'Execute the matplotlib source and persist the plot images.',
    allowedAgents: ['builder'],
    allowedStudioKinds: ['matplotlib'],
    execute: async (input, context) =>
      executePlotRenderTool(input, context as StudioRuntimeBackedToolContext, driver)
  }
}

async function executePlotRenderTool(
  input: PlotRenderToolInput,
  context: StudioRuntimeBackedToolContext,
  driver: StudioRenderDriver
): Promise<StudioToolResult> {
  if (!input.concept?.trim() || !input.code?.trim()) {
    throw new Error('Render tool requires non-empty "concept" and "code"')
  }

  const title = `Plot render: ${input.concept.slice(0, 80)}`

  return executeStudioRenderLifecycle({
    context,
    createRender: () => {
      const render = createStudioRender({
        ownerId: context.session.ownerId,
        sessionId: context.session.id,
        runId: context.run.id,
        // The Run is the authoritative scope source; a Legacy Run leaves the key absent.
        sceneId: context.run.sceneId,
        kind: 'matplotlib',
        title,
        concept: input.concept,
        outputMode: 'image',
        quality: 'medium',
        status: 'running',
        metadata: {
          studioKind: 'matplotlib',
          outputMode: 'image'
        },
      })
      render.metadata = { ...(render.metadata ?? {}), renderId: render.id }
      return render
    },
    execute: async (render, renderStore) => {
      context.setToolMetadata?.({
        title,
        metadata: {
          renderId: render.id,
          studioKind: 'matplotlib'
        }
      })

      const result = await driver.execute({
        render,
        workspaceDirectory: context.session.directory,
        code: input.code,
        signal: context.abortSignal,
      })
      throwIfStudioRunCancelled(context.abortSignal)
      const attachments = result.attachments ?? []
      const completed = await renderStore.update(context.session.ownerId, render.id, {
        status: 'completed',
        attachments,
        metadata: {
          ...(render.metadata ?? {}),
          ...(result.metadata ?? {}),
        },
      })
      if (!completed) {
        throw new Error('Render record disappeared before completion')
      }
      publishStudioRenderUpdated(context.eventBus, completed)

      return {
        title,
        output: `version: ${render.metadata?.label}\noutput_directory: ${render.metadata?.workspaceOutputDirectory}\nplot_render_id: ${render.id}`,
        attachments,
        metadata: {
          renderId: render.id,
          phase: 'render',
          diagnosticCount: 0,
          imageCount: result.metadata?.imageCount,
          scriptPath: completed.metadata?.scriptPath,
          imagePaths: completed.metadata?.imagePaths,
        },
      }
    },
    markFailed: async (render, renderStore, error) => {
      const failedRender = await renderStore.update(context.session.ownerId, render.id, {
        status: studioRenderFailureStatus(error),
        error: studioRenderFailureMessage(error),
      })
      if (failedRender) {
        publishStudioRenderUpdated(context.eventBus, failedRender)
      }
    }
  })
}