From df79d3beba9b1bfd7ab20d42e4e36ef50deb9f4e Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 29 Oct 2026 09:49:38 -0700 Subject: [PATCH] ADFA-6836 | Let create_file take empty content The required-argument check read content="" as missing, so "add a file with an empty class" failed with "Missing required argument(s): content" before reaching the approval dialog. For create_file, content now counts as missing only when it is absent. An omitted content is still rejected. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Vh2DAaJxvRXKRYkgiDqwGQ --- .../plugins/aicore/tool/Executor.kt | 15 +++++-- .../plugins/aicore/tool/ExecutorTest.kt | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/plugins/AI-Core/src/main/kotlin/com/itsaky/androidide/plugins/aicore/tool/Executor.kt b/plugins/AI-Core/src/main/kotlin/com/itsaky/androidide/plugins/aicore/tool/Executor.kt index 6112ee82..bc2d1f00 100644 --- a/plugins/AI-Core/src/main/kotlin/com/itsaky/androidide/plugins/aicore/tool/Executor.kt +++ b/plugins/AI-Core/src/main/kotlin/com/itsaky/androidide/plugins/aicore/tool/Executor.kt @@ -58,13 +58,22 @@ class Executor( private val VERBATIM_TEXT_ARGS = setOf("old_string") /** - * Whether [value] fails the required-argument check for [key]. + * Required args where an empty string is a legal value, so only an absent argument is + * missing: `create_file` with `content = ""` is an empty file, and a small model asked for + * one sends exactly that. + */ + private val EMPTY_ALLOWED_ARGS = setOf("create_file" to "content") + + /** + * Whether [value] fails the required-argument check for [key] of [toolName]. + * @param toolName the tool being dispatched. * @param key the required argument's name. * @param value the supplied value, or null when absent. * @return true when the argument must be reported missing. */ - private fun isMissing(key: String, value: Any?): Boolean { + private fun isMissing(toolName: String, key: String, value: Any?): Boolean { val text = value?.toString() + if ((toolName to key) in EMPTY_ALLOWED_ARGS) return text == null return if (key in VERBATIM_TEXT_ARGS) text.isNullOrEmpty() else text?.trim().isNullOrEmpty() } } @@ -163,7 +172,7 @@ class Executor( // Check required arguments val missingArgs = requiredArgsForTool(toolName).filter { key -> - isMissing(key, normalizedArgs[key]) + isMissing(toolName, key, normalizedArgs[key]) } if (missingArgs.isNotEmpty()) { val message = "Missing required argument(s): ${missingArgs.joinToString(", ")}" diff --git a/plugins/AI-Core/src/test/kotlin/com/itsaky/androidide/plugins/aicore/tool/ExecutorTest.kt b/plugins/AI-Core/src/test/kotlin/com/itsaky/androidide/plugins/aicore/tool/ExecutorTest.kt index 4dcc136d..25434c72 100644 --- a/plugins/AI-Core/src/test/kotlin/com/itsaky/androidide/plugins/aicore/tool/ExecutorTest.kt +++ b/plugins/AI-Core/src/test/kotlin/com/itsaky/androidide/plugins/aicore/tool/ExecutorTest.kt @@ -198,6 +198,46 @@ class ExecutorTest { assertTrue("a deletion must reach the handler", results.single().success) } + private class RecordingCreateFile : ToolHandler { + override val toolName = "create_file" + override val description = "fake create" + override val requiresApproval = false + override val pathArgs = listOf("file_path") + var seenArgs: Map? = null + private set + + override suspend fun execute(args: Map): ToolResult { + seenArgs = args + return ToolResult.success("created") + } + } + + @Test + fun givenCreateFileWithEmptyContent_whenExecuting_thenItReachesTheHandler() = runBlocking { + // An empty file is a normal request, and a small model asked for one sends content = "". + val handler = RecordingCreateFile() + + val results = executorFor(handler).execute( + listOf(ToolCall("create_file", mapOf("file_path" to "Util.kt", "content" to ""))) + ) + + assertTrue("empty content must not read as missing: ${results.single().message}", results.single().success) + assertEquals("", handler.seenArgs?.get("content")) + } + + @Test + fun givenCreateFileWithNoContentArgument_whenExecuting_thenItIsRejectedBeforeTheHandlerRuns() = runBlocking { + val handler = RecordingCreateFile() + + val results = executorFor(handler).execute( + listOf(ToolCall("create_file", mapOf("file_path" to "Util.kt"))) + ) + + assertFalse(results.single().success) + assertTrue(results.single().message.contains("content")) + assertEquals(null, handler.seenArgs) + } + @Test fun givenAWhitespaceOnlyOldString_whenExecuting_thenItReachesTheHandler() = runBlocking { // Re-indenting is a legal edit whose old_string is whitespace, not a missing argument.