AgentToolRegistrations.kt
src/jvmMain/kotlin/afield/ai/tools/AgentToolRegistrations.kt · 270 lines
package afield.ai.tools
import afield.ai.models.AnthropicBridge
import afield.ai.models.OpenAiIdeCodexBridge
import afield.ai.retrieval.RelatedContextDiscovery
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive
internal fun registerBuiltInAgentTools(tools: ToolRegistry)
Registers the full IDE agent tool set (attachments, Kotlin tools, macros, CodeUse,
semantic retrieval, agent-memory, workspace symbol search, Commentary) on tools.
Provider-independent: every handler references only the ToolExecutionContext closures,
so both OpenAiIdeCodexBridge and AnthropicBridge populate identical registries from here.
body — 253 lines, from line 16
internal fun registerBuiltInAgentTools(tools: ToolRegistry) {
// ---- code outside core -------------------------------------------------------------
// Whatever registered itself through AgentToolExtensions: personal packages, a document's own
// boxes, anything. Core deliberately does not name a domain cluster here — it used to name one
// directly, and moving that cluster into src/personal made the compiler reject the reference.
// Inverting it was the right fix independent of the move; see AgentToolExtensions.
AgentToolExtensions.applyTo(tools)
registerDocumentGuideTools(tools)
registerPresentationTools(tools)
registerTutorialCodeTools(tools)
registerDocumentSourceTools(tools)
registerStateTools(tools)
afield.ai.agent.registerFieldGraphTools(tools)
// One tool whose argument is a program over all the others; see ToolProgramTools.kt.
afield.ai.tools.program.registerToolProgramTools(tools)
// The loader for DEFERRED tools (ToolMeta.kt): their names are in the catalogue, their schemas
// arrive only when asked for, and then stay for the conversation.
tools.register(DescribeToolsRequest::class.java, { ctx -> ctx.promoted != null && tools.deferredUnder(ctx).isNotEmpty() }) { ctx, req ->
val loadable = tools.describe(ctx, ToolFace.PROGRAM).mapNotNull { it as? kotlinx.serialization.json.JsonObject }
.filter { d ->
val name = d["name"]?.jsonPrimitive?.contentOrNull ?: return@filter false
tools.metaOf(ctx, name)?.exposure.let { it == ToolExposure.DEFERRED || it == ToolExposure.PROGRAM }
}
fun nameOf(d: kotlinx.serialization.json.JsonObject) = d["name"]!!.jsonPrimitive.content
val asked = req.names.map { it.trim() }.filter { it.isNotEmpty() }.toSet()
val chosen = loadable.filter { d ->
val name = nameOf(d)
(req.namespace.isNotBlank() && tools.metaOf(ctx, name)?.namespace == req.namespace.trim()) ||
name in asked || "${name.removeSuffix("Request")}" in asked
}
if (chosen.isEmpty()) {
DescribeToolsResult(false, "Nothing matched namespace='${req.namespace}' names=${req.names}. " +
"Loadable: " + loadable.joinToString { nameOf(it) }, emptyList())
} else {
ctx.promoted?.addAll(chosen.map(::nameOf))
DescribeToolsResult(true, "Loaded ${chosen.size} tool(s); call them as ordinary tools from now on.", chosen.map { d ->
val meta = tools.metaOf(ctx, nameOf(d)) ?: ToolMeta.DEFAULT
DescribedTool(nameOf(d), meta.namespace, d["description"]?.jsonPrimitive?.contentOrNull.orEmpty(),
meta.effects.map { it.name }.sorted(), d["inputSchema"].toString())
})
}
}
afield.ai.agent.registerGeometryReviewTools(tools)
afield.ai.agent.registerStudyBuildTools(tools)
registerAskUserTools(tools)
registerRepoFileTools(tools)
registerRepoCommandTools(tools)
afield.ai.agent.registerKitTools(tools)
// Commentary: handler can be a no-op acknowledgement; we extract text from call args anyway.
tools.register(Commentary::class.java) { _, _ -> mapOf("ok" to true) }
// Attachments
tools.register(ListAttachmentsRequest::class.java, { ctx -> ctx.attachments.isNotEmpty() || ctx.resolveAttachment != null }) { ctx, _ ->
ListAttachmentsResult(
attachments = ctx.attachments.map { AttachmentInfo(it.fileName, it.language, it.description) }
)
}
tools.register(AttachmentRequest::class.java, { ctx -> ctx.attachments.isNotEmpty() || ctx.resolveAttachment != null }) { ctx, req ->
val requested = req.file.trim()
if (requested.isEmpty()) {
throw IllegalArgumentException("Attachment name is empty.")
}
fun normalizePath(value: String): String = value.replace('\\', '/').trim()
val normalizedRequested = normalizePath(requested)
val hit = ctx.attachments.firstOrNull { it.fileName == requested }
?: ctx.attachments.firstOrNull { normalizePath(it.fileName) == normalizedRequested }
?: ctx.attachments.firstOrNull {
val normalizedCandidate = normalizePath(it.fileName)
normalizedCandidate.endsWith("/$normalizedRequested")
}
val resolved = if (hit != null) {
val related = RelatedContextDiscovery.forPath(hit.fileName)
AttachmentResponse(
file = hit.fileName,
language = hit.language,
content = hit.code,
relatedGuides = related.guides,
relatedExamples = related.examples
)
} else {
// WorkspaceSearch's FIELD_KOTLIN_SOURCE paths are relative to src/; accept them as given.
ctx.resolveAttachment?.invoke(requested)
?: ctx.resolveAttachment?.takeIf { !normalizedRequested.startsWith("/") && !normalizedRequested.startsWith("src/") }
?.invoke("src/$normalizedRequested")
?: throw IllegalArgumentException(
"No attachment or resolvable file named '$requested'. Call ListAttachments first."
)
}
resolved.excerpt(req)
}
// Kotlin-only tools (only work if provider is supplied in ctx)
tools.register(ListDefinitionsRequest::class.java, { ctx -> ctx.listDefinitions != null }) { ctx, _ ->
ctx.listDefinitions?.invoke()
?: throw IllegalStateException("ListDefinitions not available for this request.")
}
tools.register(ListContentsRequest::class.java, { ctx -> ctx.listFileContents != null }) { ctx, _ ->
ctx.listFileContents?.invoke()
?: throw IllegalStateException("ListContents not available for this request.")
}
tools.register(ListKotlinContextVariablesRequest::class.java, { ctx -> ctx.listKotlinContextVariables != null }) { ctx, req ->
ctx.listKotlinContextVariables?.invoke(req)
?: throw IllegalStateException("ListKotlinContextVariables not available for this request.")
}
tools.register(EvaluateKotlinExpressionRequest::class.java, { ctx -> ctx.evaluateKotlinExpression != null }) { ctx, req ->
ctx.evaluateKotlinExpression?.invoke(req)
?: throw IllegalStateException("EvaluateKotlinExpression not available for this request.")
}
tools.register(EditFileRequest::class.java, { ctx -> ctx.editOtherFile != null }) { ctx, req ->
ctx.editOtherFile?.invoke(req)
?: throw IllegalStateException("EditFile not available for this request.")
}
tools.register(CompileKotlinRequest::class.java, { ctx -> ctx.compileKotlin != null }) { ctx, req ->
ctx.compileKotlin?.invoke(req)
?: throw IllegalStateException("CompileKotlin not available for this request.")
}
tools.register(ReloadShadersRequest::class.java, { ctx -> ctx.reloadShaders != null }) { ctx, req ->
ctx.reloadShaders?.invoke(req)
?: throw IllegalStateException("ReloadShaders not available for this request.")
}
// Deliberately gated only on the provider existing, not on whether the conversation already
// happens to contain an image. Seeing the render window is the whole point of an agent editing
// graphics code, and making it conditional on prior visual context is what left agents blind.
tools.register(CaptureRenderWindowRequest::class.java, { ctx -> ctx.captureRenderWindow != null }) { ctx, req ->
ctx.captureRenderWindow?.invoke(req)
?: throw IllegalStateException("CaptureRenderWindow not available for this request.")
}
tools.register(MacroListRequest::class.java, { ctx -> ctx.listMacros != null }) { ctx, req ->
ctx.listMacros?.invoke(req)
?: throw IllegalStateException("MacroList not available for this request.")
}
tools.register(MacroReadRequest::class.java, { ctx -> ctx.readMacro != null }) { ctx, req ->
ctx.readMacro?.invoke(req)
?: throw IllegalStateException("MacroRead not available for this request.")
}
tools.register(MacroWriteRequest::class.java, { ctx -> ctx.writeMacro != null }) { ctx, req ->
ctx.writeMacro?.invoke(req)
?: throw IllegalStateException("MacroWrite not available for this request.")
}
tools.register(MacroRunRequest::class.java, { ctx -> ctx.runMacro != null }) { ctx, req ->
ctx.runMacro?.invoke(req)
?: throw IllegalStateException("MacroRun not available for this request.")
}
tools.register(CodeUseQueryRequest::class.java, { ctx -> ctx.codeUseQuery != null }) { ctx, req ->
ctx.codeUseQuery?.invoke(req)
?: throw IllegalStateException("CodeUseQuery not available for this request.")
}
tools.register(CodeUseCallersForSourceLineRequest::class.java, { ctx -> ctx.codeUseQuery != null }) { ctx, req ->
ctx.codeUseQuery?.invoke(
CodeUseQueryRequest(
kind = "line-callers",
sourcePath = req.sourcePath,
line = req.line,
limit = req.limit
)
) ?: throw IllegalStateException("CodeUseCallersForSourceLine not available for this request.")
}
tools.register(CodeUseReferencesForCodeRequest::class.java, { ctx -> ctx.codeUseQuery != null }) { ctx, req ->
ctx.codeUseQuery?.invoke(
CodeUseQueryRequest(
kind = "references-for-code",
code = req.code,
limit = req.limit
)
) ?: throw IllegalStateException("CodeUseReferencesForCode not available for this request.")
}
tools.register(CodeUseRelatedSourceForCodeRequest::class.java, { ctx -> ctx.codeUseQuery != null }) { ctx, req ->
ctx.codeUseQuery?.invoke(
CodeUseQueryRequest(
kind = "related-source-for-code",
code = req.code,
limit = req.limit,
callersPerSymbol = req.callersPerSymbol,
sourceRadiusLines = req.sourceRadiusLines
)
) ?: throw IllegalStateException("CodeUseRelatedSourceForCode not available for this request.")
}
tools.register(CodeUseSemanticAnalyzeCodeRequest::class.java, { ctx -> ctx.codeUseQuery != null }) { ctx, req ->
ctx.codeUseQuery?.invoke(
CodeUseQueryRequest(
kind = "semantic-analyze",
code = req.code,
sourceName = req.sourceName,
limit = req.limit,
callersPerSymbol = req.callersPerSymbol,
sourceRadiusLines = req.sourceRadiusLines
)
) ?: throw IllegalStateException("CodeUseSemanticAnalyzeCode not available for this request.")
}
tools.register(CodeUseHotSymbolsRequest::class.java, { ctx -> ctx.codeUseQuery != null }) { ctx, req ->
ctx.codeUseQuery?.invoke(
CodeUseQueryRequest(
kind = "hot-symbols",
window = req.window,
limit = req.limit
)
) ?: throw IllegalStateException("CodeUseHotSymbols not available for this request.")
}
tools.register(CodeUseEarlyDocumentSymbolsRequest::class.java, { ctx -> ctx.codeUseQuery != null }) { ctx, req ->
ctx.codeUseQuery?.invoke(
CodeUseQueryRequest(
kind = "early-document-symbols",
document = req.document,
earlyWindowMs = req.earlyWindowMs,
limit = req.limit
)
) ?: throw IllegalStateException("CodeUseEarlyDocumentSymbols not available for this request.")
}
tools.register(SemanticRetrievalSearchRequest::class.java, { ctx -> ctx.semanticRetrievalSearch != null }) { ctx, req ->
ctx.semanticRetrievalSearch?.invoke(req)
?: throw IllegalStateException("SemanticRetrievalSearch not available for this request.")
}
tools.register(SemanticRetrievalStatusRequest::class.java, { ctx -> ctx.semanticRetrievalStatus != null }) { ctx, _ ->
ctx.semanticRetrievalStatus?.invoke()
?: throw IllegalStateException("SemanticRetrievalStatus not available for this request.")
}
tools.register(SemanticRetrievalRebuildRequest::class.java, { ctx -> ctx.semanticRetrievalRebuild != null }) { ctx, _ ->
ctx.semanticRetrievalRebuild?.invoke()
?: throw IllegalStateException("SemanticRetrievalRebuild not available for this request.")
}
tools.register(ScratchpadReadRequest::class.java, { ctx -> ctx.agentMemory != null }) { ctx, _ ->
ctx.agentMemory?.readScratchpad()
?: throw IllegalStateException("Scratchpad not available for this request.")
}
tools.register(ScratchpadUpdateRequest::class.java, { ctx -> ctx.agentMemory != null }) { ctx, req ->
ctx.agentMemory?.updateScratchpad(req)
?: throw IllegalStateException("Scratchpad not available for this request.")
}
tools.register(ChecklistReadRequest::class.java, { ctx -> ctx.agentMemory != null }) { ctx, _ ->
ctx.agentMemory?.readChecklist()
?: throw IllegalStateException("Checklist not available for this request.")
}
tools.register(ChecklistUpdateRequest::class.java, { ctx -> ctx.agentMemory != null }) { ctx, req ->
ctx.agentMemory?.updateChecklist(req)
?: throw IllegalStateException("Checklist not available for this request.")
}
tools.register(WorkspaceSearchRequest::class.java, { it.workspaceSearch != null }) { ctx, req ->
ctx.workspaceSearch!!.invoke(req)
}
// Optional extra tool
tools.register(WorkspaceSymbolSearch::class.java, { ctx -> ctx.workspaceSymbolSearch != null }) { ctx, req ->
ctx.workspaceSymbolSearch?.invoke(req)
?: throw IllegalStateException("WorkspaceSymbolSearch not available for this request.")
}
}