Skip to content

Ai4RuntimeToolProviders.kt

src/jvmMain/kotlin/afield/ai/tools/Ai4RuntimeToolProviders.kt · 613 lines

package and imports — 94 lines, from line 1
package afield.ai.tools

import afield.ai.models.FileRef
import afield.ai.models.boxLabel
import afield.ai.models.clipToolText
import afield.ai.models.fileRefFor
import afield.ai.models.primitiveKind
import afield.ai.models.primitiveToolValue
import afield.ai.models.runtimeTypeName
import afield.ai.models.safeString

import afield.ai.agent.*
import afield.ai.conversation.*
import afield.ai.tools.*
import afield.animation.gesture.*
import afield.animation.interpolation.*
import afield.animation.keyframes.*
import afield.app.diagnostics.*
import afield.app.notifications.*
import afield.app.protocol.*
import afield.app.server.*
import afield.app.settings.*
import afield.audio.synth.*
import afield.data.table.*
import afield.document.context.*
import afield.document.graph.*
import afield.document.history.*
import afield.document.persistence.*
import afield.document.properties.*
import afield.document.workspace.*
import afield.graphics.color.*
import afield.graphics.paint.*
import afield.graphics.vulkan.*
import afield.math.functions.*
import afield.media.capture.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.completion.*
import afield.runtime.execution.*
import afield.runtime.kotlin.*
import afield.runtime.lifecycle.*
import afield.tools.automation.*
import afield.tools.docs.*
import afield.ui.controls.*
import afield.ui.layout.*
import afield.ui.notation.*
import afield.ui.output.*
import afield.ui.output.templates.*
import afield.ui.slides.*
import afield.ui.visualization.*
import afield.runtime.completion.KotlinSourceIndex
import afield.ai.tools.searchWorkspaceSymbols
import afield.app.diagnostics.logging.lprintln
import afield.ai.learning.*
import afield.ai.retrieval.*
import afield.animation.motion.*
import afield.app.commands.*
import afield.audio.analysis.*
import afield.data.collections.*
import afield.data.formats.*
import afield.data.graph.*
import afield.data.sorting.*
import afield.geometry.mesh.*
import afield.geometry.path.*
import afield.geometry.spatial.*
import afield.graphics.interaction.*
import afield.graphics.render.*
import afield.graphics.shader.*
import afield.graphics.text.*
import afield.graphics.vulkan.buffers.*
import afield.io.osc.*
import afield.io.process.*
import afield.lang.lambda.*
import afield.lang.python.*
import afield.lang.remotejs.*
import afield.lang.stacksl.*
import afield.math.autodiff.*
import afield.math.control.*
import afield.math.estimation.*
import afield.math.linalg.*
import afield.math.optimization.*
import afield.math.statistics.*
import afield.math.units.*
import afield.media.image.*
import afield.runtime.languages.*
import afield.simulation.agents.*
import afield.simulation.automata.*
import afield.simulation.particles.*
import afield.study.build.*
import afield.study.evidence.*
import afield.study.search.*
import afield.study.source.*
import afield.study.ui.*
import kotlinx.coroutines.*

The five AI4 tools that need the running Field rather than the saved document: reload a box's shaders, capture the render window, list a box's Kotlin context variables, query the code-use index, and search workspace symbols.

The in-Field chat panel and the backdoor registry share all five and differ only in what they hand the constructor. The caller supplies how a box reference resolves and how to look a box up by uid, which is what lets the panel mean "the box whose chat is open" where the backdoor insists on an explicit target. The panel also supplies a staged-source hook, so a shader reload compiles the text the editor is showing rather than the text last written to the box; the backdoor leaves that argument out and compiles what the box holds.

Shader reload is the largest of the five, because one box can carry several shader programs at once and a request may not say which to rebuild. An explicit mode wins outright. Failing that the property being reloaded implies one: vertex and fragment mean graphics, compute means compute, task and mesh mean mesh, and the ray-tracing stages mean ray. Failing that too, the box is asked which programs it actually has and each is rebuilt, which is what auto and an absent mode both come to.

internal class Ai4RuntimeToolProviders

Shared runtime providers. The caller supplies targeting and provisional source policy.

internal class Ai4RuntimeToolProviders(
    private val sessionInfo: SessionInformation,
    private val refResolver: (String, String) -> FileRef,
    private val boxByIdOrNull: (String) -> Box?,
    private val applyStagedShaderSourcesForReload: (Box) -> (() -> Unit) = { {} }
) {
    private val docName = java.io.File(sessionInfo.filename).name

fun modeForShaderProperty(property: String): String?

    fun modeForShaderProperty(property: String): String? = when (property) {
        "vertex", "fragment", "vertex_fragment", "vertex_s", "fragment_s" -> "graphics"
        "compute", "compute_s" -> "compute"
        "task", "mesh", "task_s", "mesh_s" -> "mesh"
        "raygen", "miss", "closesthit", "anyhit", "intersection", "callable",
        "raygen_s", "miss_s", "closesthit_s", "anyhit_s", "intersection_s", "callable_s" -> "ray"
        else -> null
    }

fun modesForShaderReload(target: Box, requestedMode: String, requestedProperty: String): List<String>

    fun modesForShaderReload(target: Box, requestedMode: String, requestedProperty: String): List<String> {
        val normalized = requestedMode.trim().lowercase()
        if (normalized !in setOf("", "auto")) return listOf(
            when (normalized) {
                "graphics", "graphic", "module", "vertex", "fragment" -> "graphics"
                "compute" -> "compute"
                "mesh", "task" -> "mesh"
                "ray", "rays", "raytracing", "ray-tracing", "ray_tracing" -> "ray"
                else -> normalized
            }
        )

        modeForShaderProperty(requestedProperty)?.let { return listOf(it) }

        return buildList {
            if (runCatching { target._shader != null }.getOrDefault(false)) add("graphics")
            if (runCatching { target._computeShader != null }.getOrDefault(false)) add("compute")
            if (runCatching { target._meshShader != null }.getOrDefault(false)) add("mesh")
            if (runCatching { target._rayTracingShader != null }.getOrDefault(false)) add("ray")
        }
    }

val reloadShaders: (ReloadShadersRequest) -> ReloadShadersResult = reloadShaders@{ req ->

body — 157 lines, from line 155
    val reloadShaders: (ReloadShadersRequest) -> ReloadShadersResult = reloadShaders@{ req ->
        val ref = refResolver(req.target, "code")
        if (ref.document != docName) {
            return@reloadShaders ReloadShadersResult(
                success = false,
                target = req.target,
                mode = req.mode,
                message = "Unknown document '${ref.document}'."
            )
        }
        val target = boxByIdOrNull(ref.uid)
            ?: return@reloadShaders ReloadShadersResult(
                success = false,
                target = req.target,
                mode = req.mode,
                message = "Unknown box '${ref.uid}'."
            )

        val modes = modesForShaderReload(target, req.mode, ref.property).distinct()
        if (modes.isEmpty()) {
            return@reloadShaders ReloadShadersResult(
                success = false,
                target = fileRefFor(docName, target.id, ref.property),
                mode = req.mode,
                message = "No shader module pointer is attached to '${target.id}'."
            )
        }

        notify("Reloading shader(s): ${modes.joinToString(", ")}", "AI4")
        val errors = mutableListOf<ShaderReloadError>()
        val reloaded = mutableListOf<String>()
        val failed = mutableListOf<String>()
        val oldErrorProxy = target.output.errorProxy
        val restoreShaderSources = applyStagedShaderSourcesForReload(target)

        try {
            runCatching {
                runBlocking {
                    withContext(Runloop.runLoop.main) {
                        target.output.errorProxy = { content, property, line ->
                            if (content != null) {
                                errors += ShaderReloadError(
                                    property = property.ifBlank { "shader" },
                                    line = line,
                                    message = content.toString()
                                )
                            }
                            oldErrorProxy(content, property, line)
                        }
                        try {
                            modes.forEach { mode ->
                                val ok = when (mode) {
                                    "graphics" -> if (runCatching { target._shader != null }.getOrDefault(false)) {
                                        reloadShader(target)
                                    } else {
                                        false
                                    }
                                    "compute" -> if (runCatching { target._computeShader != null }.getOrDefault(false)) {
                                        reloadComputeShader(target)
                                    } else {
                                        false
                                    }
                                    "mesh" -> if (runCatching { target._meshShader != null }.getOrDefault(false)) {
                                        reloadMeshShader(target)
                                    } else {
                                        false
                                    }
                                    "ray" -> if (runCatching { target._rayTracingShader != null }.getOrDefault(false)) {
                                        reloadRayTracingShader(target)
                                    } else {
                                        false
                                    }
                                    else -> {
                                        errors += ShaderReloadError(mode, message = "Unknown shader reload mode '$mode'.")
                                        false
                                    }
                                }
                                if (ok) reloaded += mode else failed += mode
                            }
                        } finally {
                            target.output.errorProxy = oldErrorProxy
                        }
                    }
                }
            }.onFailure { throwable ->
                failed += modes.filter { it !in reloaded }
                errors += ShaderReloadError(
                    property = "shader",
                    message = throwable.message ?: throwable::class.java.name
                )
                target.output.errorProxy = oldErrorProxy
            }
        } finally {
            restoreShaderSources()
        }

        val success = failed.isEmpty() && errors.isEmpty()
        var visualAttached = false
        var visualMessage = ""
        var visualDataUrl = ""
        // `visualVerificationAvailable()` used to gate this too, and it was the whole bug: it asks
        // whether the conversation *already* contains a `data:image/`, which an agent has no way to
        // arrange for itself. The only two things that put one there are a person attaching an image
        // and a person typing `/screenshot`, so a fresh chat could never see the render window, and
        // `visualCheck=true` was refused for the life of the conversation. Worse, it was refused
        // with `visualCheckMessage` left empty, so the model got `visualCheckAttached=false` and no
        // reason at all — which is indistinguishable from a capture that was attempted and failed.
        //
        // Every branch below now leaves a message behind. A refusal that cannot explain itself sends
        // the agent off guessing, and that is exactly what happened.
        if (req.visualCheck && success) {
            runCatching {
                runBlocking {
                    val screenshot = captureChatScreenshot(
                        AiChatScreenshotRequest(uid = target.id, target = ""),
                        sessionInfo
                    )
                    visualMessage = screenshot.message
                    if (screenshot.success && screenshot.dataUrl.isNotBlank()) {
                        visualAttached = true
                        visualDataUrl = screenshot.dataUrl
                    }
                }
            }.onFailure { throwable ->
                visualMessage = throwable.message ?: throwable::class.java.simpleName
            }
        } else if (req.visualCheck) {
            // Still conditional on the reload having worked, but for a real reason: this is a
            // *post-reload* screenshot, and after a failed reload the window still shows the old
            // shader, so attaching it would be actively misleading. Name the other tool, because
            // "look at it as it stands" is a legitimate thing to want here.
            visualMessage = "Not captured: the reload did not succeed, so the window still shows the " +
                "previous shader and a screenshot would be misleading. Fix the errors and reload " +
                "again, or call CaptureRenderWindow to see the window as it currently stands."
        }

        val message = buildString {
            append(if (success) "Shader reload succeeded" else "Shader reload failed")
            append(" for ${target.id}")
            if (reloaded.isNotEmpty()) append("; reloaded=${reloaded.joinToString(",")}")
            if (failed.isNotEmpty()) append("; failed=${failed.joinToString(",")}")
            if (visualMessage.isNotBlank()) append("; visual=$visualMessage")
        }
        notify(message, "AI4")

        ReloadShadersResult(
            success = success,
            target = fileRefFor(docName, target.id, ref.property),
            mode = req.mode,
            message = message,
            reloaded = reloaded,
            errors = errors,
            visualCheckAttached = visualAttached,
            visualCheckMessage = visualMessage,
            visualDataUrl = visualDataUrl
        )
    }

val captureRenderWindow: (CaptureRenderWindowRequest) -> CaptureRenderWindowResult = { req ->

Look at the Field 4 render window, on demand.

Separate from reloadShaders' visualCheck because that one is a post-reload check: it only means anything when a reload has just succeeded, and it presupposes there was a shader edit at all. An agent doing graphics work wants to look before it changes anything, and wants to look when a reload has just failed. Until this tool existed the only route to the window was a successful shader reload, so an agent asked to inspect a scene had to call ReloadShaders on a shader it had not modified and hope — which is precisely what the logs show one doing, twice, before giving up and reporting that no image had been attached.

Ungated on purpose. See the note on the gate in reloadShaders above.

    val captureRenderWindow: (CaptureRenderWindowRequest) -> CaptureRenderWindowResult = { req ->
        val settle = req.settleMs.coerceIn(0, CAPTURE_RENDER_WINDOW_MAX_SETTLE_MS)
        // requestedRefOrCurrent already handles all three spellings: blank (this chat's box), an
        // afield:// ref, and a bare uid.
        runCatching {
            val ref = refResolver(req.uid, "code")
            require(ref.document == docName) { "Capture must target the attached document '$docName'" }
            val uid = ref.uid
            runBlocking {
                if (settle > 0) delay(settle.toLong())
                val shot = captureChatScreenshot(
                    AiChatScreenshotRequest(uid = uid, target = req.target),
                    sessionInfo
                )
                CaptureRenderWindowResult(
                    success = shot.success && shot.dataUrl.isNotBlank(),
                    message = shot.message.ifBlank {
                        if (shot.success) "Captured the render window." else "Capture failed."
                    },
                    title = shot.title,
                    width = shot.width,
                    height = shot.height,
                    settledMs = settle,
                    dataUrl = if (shot.success) shot.dataUrl else ""
                )
            }
        }.getOrElse { throwable ->
            val why = throwable.message ?: throwable::class.java.simpleName
            lprintln("warning,ai4", "CaptureRenderWindow failed for uid '${req.uid}': $why")
            CaptureRenderWindowResult(success = false, message = why, settledMs = settle)
        }
    }

val codeUseQuery: (CodeUseQueryRequest) -> CodeUseQueryResult = codeUseQuery@{ req ->

body — 196 lines, from line 360
    val codeUseQuery: (CodeUseQueryRequest) -> CodeUseQueryResult = codeUseQuery@{ req ->
        val kind = req.kind.trim().lowercase().ifBlank { "failures" }
        val limit = req.limit.coerceIn(1, 50)
        val fetchLimit = (limit * 5).coerceIn(limit, 500)
        val documentFilter = req.document.trim()
        val boxFilter = req.box.trim()

        fun matches(document: String, boxUid: String, boxName: String): Boolean {
            if (documentFilter.isNotBlank() && !document.contains(documentFilter, ignoreCase = true)) return false
            if (boxFilter.isNotBlank()) {
                val hit = boxUid.contains(boxFilter, ignoreCase = true) ||
                    boxName.contains(boxFilter, ignoreCase = true)
                if (!hit) return false
            }
            return true
        }

        // A bounded lookup must not wait for whole-database summary scans. The
        // snapshot carries its own gathering/staleness message and refreshes in the background.
        when (kind) {
            "summary", "status" -> {
                val summary = CodeUseTelemetryService.summarySnapshot()
                CodeUseQueryResult(
                    success = summary.enabled,
                    kind = "summary",
                    message = if (summary.enabled) {
                        "Local code-use history is enabled; snippets=${summary.snippets}, failures available via kind=failures."
                    } else {
                        summary.message.ifBlank { "Local code-use history is disabled." }
                    },
                    summary = summary
                )
            }

            "failure", "failures", "error", "errors", "exceptions" -> {
                val rows = CodeUseTelemetryService.failures(fetchLimit)
                    .filter { matches(it.document, it.boxUid, it.boxName) }
                    .take(limit)
                CodeUseQueryResult(
                    success = true,
                    kind = "failures",
                    message = "Returned ${rows.size} retained compiler/runtime failure row(s).",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    failures = rows
                )
            }

            "snippet", "snippets", "timeline" -> {
                val rows = CodeUseTelemetryService.recentSnippets(fetchLimit)
                    .filter { matches(it.document, it.boxUid, it.boxName) }
                    .take(limit)
                CodeUseQueryResult(
                    success = true,
                    kind = "snippets",
                    message = "Returned ${rows.size} recent snippet row(s).",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    snippets = rows
                )
            }

            "launch", "launches", "lifecycle", "lifecycles" -> {
                val rows = CodeUseTelemetryService.recentLaunches(fetchLimit)
                    .filter { matches(it.document, it.boxUid, it.boxName) }
                    .take(limit)
                CodeUseQueryResult(
                    success = true,
                    kind = "launches",
                    message = "Returned ${rows.size} launch lifecycle row(s).",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    launches = rows
                )
            }

            "symbol", "symbols" -> {
                val rows = CodeUseTelemetryService.symbols(limit)
                CodeUseQueryResult(
                    success = true,
                    kind = "symbols",
                    message = "Returned ${rows.size} symbol row(s). Symbol rows are populated once the analyzer has resolved uses.",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    symbols = rows
                )
            }

            "unresolved" -> {
                val rows = CodeUseTelemetryService.unresolved(limit)
                CodeUseQueryResult(
                    success = true,
                    kind = "unresolved",
                    message = "Returned ${rows.size} unresolved analyzer row(s).",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    unresolved = rows
                )
            }

            "line-callers", "callers", "callers-for-source-line", "source-line-callers" -> {
                val (target, rows) = CodeUseTelemetryService.callersForSourceLine(
                    sourcePath = req.sourcePath,
                    line = req.line,
                    limit = limit
                )
                CodeUseQueryResult(
                    success = target != null,
                    kind = "line-callers",
                    message = if (target != null) {
                        "Returned ${rows.size} recent Field snippet caller row(s) for ${target.symbolName} at ${target.sourceFile}:${target.line}."
                    } else {
                        "Could not resolve a source symbol for '${req.sourcePath}:${req.line}'."
                    },
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    target = target,
                    callers = rows
                )
            }

            "references", "references-for-code", "code-references", "refs" -> {
                val rows = CodeUseTelemetryService.referencesForCode(req.code, limit.coerceAtMost(10))
                CodeUseQueryResult(
                    success = true,
                    kind = "references-for-code",
                    message = "Returned ${rows.size} likely Field source reference row(s).",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    references = rows
                )
            }

            "related-source", "related-source-for-code", "source-bundle", "source-context" -> {
                val rows = CodeUseTelemetryService.relatedSourceForCode(
                    code = req.code,
                    symbolLimit = limit.coerceAtMost(10),
                    callersPerSymbol = req.callersPerSymbol,
                    sourceRadiusLines = req.sourceRadiusLines
                )
                CodeUseQueryResult(
                    success = true,
                    kind = "related-source-for-code",
                    message = "Returned ${rows.size} related Field source bundle row(s) with excerpts, usage counts, and peer callers.",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    relatedSources = rows
                )
            }

            "semantic", "semantic-analyze", "semantic-analysis", "analyze-code", "semantic-references" -> {
                val analysis = CodeUseTelemetryService.semanticAnalyzeCode(
                    CodeUseSemanticAnalyzeRequest(
                        code = req.code,
                        sourceName = req.sourceName,
                        limit = limit.coerceAtMost(10),
                        callersPerSymbol = req.callersPerSymbol,
                        sourceRadiusLines = req.sourceRadiusLines
                    )
                )
                CodeUseQueryResult(
                    success = analysis.success,
                    kind = "semantic-analyze",
                    message = analysis.message,
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    references = analysis.references,
                    relatedSources = analysis.relatedSources,
                    semanticAnalysis = analysis
                )
            }

            "hot", "hot-symbols", "hot-source-symbols" -> {
                val rows = CodeUseTelemetryService.hotSourceSymbols(req.window, limit)
                CodeUseQueryResult(
                    success = true,
                    kind = "hot-symbols",
                    message = "Returned ${rows.size} hot source symbol row(s) for window='${req.window}'. Confidence values are literal: k2-resolved is semantic; source-index values are conservative static matches; textual values are fallback evidence.",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    hotSymbols = rows
                )
            }

            "early", "early-document-symbols", "open-time", "open-only" -> {
                val rows = CodeUseTelemetryService.earlyDocumentSymbols(
                    document = req.document,
                    earlyWindowMs = req.earlyWindowMs,
                    limit = limit
                )
                CodeUseQueryResult(
                    success = true,
                    kind = "early-document-symbols",
                    message = "Returned ${rows.size} source symbol row(s) used early in a document and rarely afterward.",
                    summary = CodeUseTelemetryService.summarySnapshot(),
                    earlySymbols = rows
                )
            }

            else -> CodeUseQueryResult(
                success = false,
                kind = kind,
                message = "Unknown kind '${req.kind}'. Use summary, failures, snippets, launches, symbols, unresolved, line-callers, references-for-code, related-source-for-code, semantic-analyze, hot-symbols, or early-document-symbols."
            )
        }
    }

val listKotlinContextVariables: (ListKotlinContextVariablesRequest) -> ListKotlinContextVariablesResult =

body — 49 lines, from line 558
    val listKotlinContextVariables: (ListKotlinContextVariablesRequest) -> ListKotlinContextVariablesResult =
        listKotlinContextVariables@{ req ->
            val ref = refResolver(req.target, "code")
            if (ref.document != docName) {
                return@listKotlinContextVariables ListKotlinContextVariablesResult(
                    success = false,
                    target = req.target,
                    boxUid = ref.uid,
                    boxName = "",
                    generation = 0,
                    message = "Unknown document '${ref.document}'."
                )
            }
            val target = boxByIdOrNull(ref.uid)
                ?: return@listKotlinContextVariables ListKotlinContextVariablesResult(
                    success = false,
                    target = req.target,
                    boxUid = ref.uid,
                    boxName = "",
                    generation = 0,
                    message = "Unknown box '${ref.uid}'."
                )

            val variables = target.ki.context.vars.values
                .sortedBy { it.name }
                .map { info ->
                    val value = info.value
                    KotlinContextVariableInfo(
                        name = info.name,
                        declaration = runCatching { shortenType(info.descriptor.toString()) }.getOrDefault(info.type),
                        declaredType = runCatching { shortenType(info.descriptor.returnType.toString()) }.getOrDefault(""),
                        runtimeType = runtimeTypeName(value),
                        mutable = info.descriptor is kotlin.reflect.KMutableProperty<*>,
                        valuePreview = clipToolText(safeString(value), req.maxValueChars),
                        primitiveValue = primitiveToolValue(value, req.maxValueChars),
                        primitiveKind = primitiveKind(value)
                    )
                }

            ListKotlinContextVariablesResult(
                success = true,
                target = fileRefFor(docName, target.id, ref.property),
                boxUid = target.id,
                boxName = boxLabel(target),
                generation = target.ki.context.generation,
                message = "Found ${variables.size} Kotlin context variable(s) for ${target.id}.",
                variables = variables
            )
        }

val workspaceSymbolSearch: (WorkspaceSymbolSearch) -> WorkspaceSymbolSearchResult = { req ->

    val workspaceSymbolSearch: (WorkspaceSymbolSearch) -> WorkspaceSymbolSearchResult = { req ->
        searchWorkspaceSymbols(KotlinSourceIndex.latestIndex ?: KotlinSourceIndex.loadIndex(), req.query.trim(), req.limit, req.includeRelatedContext)
    }
}