Skip to content

Tool Execution Modes

As the tool system grew, we needed to formalize how tools execute across different environments.

Background

Tools in Trailblaze serve different purposes. Some are meant for LLM selection, while others are precise implementation details for recordings. We need a way to classify tools by their execution characteristics.

What we decided

Tools declare two boolean properties that determine their execution mode:

@TrailblazeToolClass(
    name = "tapOnElementWithText",
    isForLlm = true,       // Can LLM select this tool?
    isRecordable = true,   // Can this tool appear in recordings?
)

Property Definitions

Property Default Meaning
isForLlm true Whether the LLM can select this tool. Set to false for implementation-detail tools that use unstable identifiers.
isRecordable true Whether this tool can appear in trail recordings. Set to false for wrapper tools that delegate to more precise tools.

Tool Type Matrix

isForLlm isRecordable Type Use Case
true true Standard Normal tools (default)
true false LLM-only Wrapper tools that delegate to more precise tools
false true Recording-only Precise tools with unstable identifiers
false false Internal Helper tools, not directly usable

Use Cases

Standard (isForLlm=true, isRecordable=true): Most tools. LLM can select them, and they appear in recordings.

LLM-only (isForLlm=true, isRecordable=false): High-level tools the LLM selects, but recordings capture the delegated call instead.

// LLM selects this (stable, text-based)
@TrailblazeToolClass(name = "tapOnElementWithText", isForLlm = true, isRecordable = false)
class TapOnElementWithText : Tool {
    override fun execute(args: Args): Result {
        val nodeId = findNodeIdByText(args.text)
        return tapOnElementByNodeId.execute(nodeId)  // Recording captures this
    }
}

Recording-only (isForLlm=false, isRecordable=true): Precise tools that use unstable identifiers (node IDs, coordinates). Not LLM-selectable because the identifiers change between runs.

// Recording stores this (precise, but node IDs are unstable)
@TrailblazeToolClass(name = "tapOnElementByNodeId", isForLlm = false, isRecordable = true)
class TapOnElementByNodeId : Tool { ... }

What changed

Positive: Clean separation between LLM-facing and implementation tools; recordings can use more precise identifiers than what LLM reasons about.

Negative: Requires understanding the delegation pattern; tool authors must choose modes deliberately.