Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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(", ")}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,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<String, Any?>? = null
private set

override suspend fun execute(args: Map<String, Any?>): 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.
Expand Down