fix: implement stage_complete tool for LLM-driven stage completion

- Add STAGE_COMPLETE_TOOL constant and meta-tool handler in executeStage
  loop: when LLM calls stage_complete, break out of tool dispatch loop
  and proceed to normal validation/transition
- Inject stage_complete tool definition in every inference request so
  the LLM always has the option to signal stage completion
- Replace expand-all diff with scrollable diff window in approval
  surface (ctrl+x toggles, up/down scrolls, shows line range)
This commit is contained in:
2026-05-29 17:40:51 +04:00
parent 7c55a53a9f
commit 2127824942
5 changed files with 41 additions and 11 deletions
@@ -92,6 +92,7 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.datetime.Clock
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.slf4j.LoggerFactory
import java.util.*
import java.util.concurrent.*
@@ -99,6 +100,7 @@ import java.util.concurrent.atomic.*
import kotlin.coroutines.cancellation.CancellationException
private const val MAX_TOOL_ROUNDS = 10
private const val STAGE_COMPLETE_TOOL = "stage_complete"
@SuppressWarnings(
"ForbiddenComment",
@@ -258,6 +260,12 @@ abstract class SessionOrchestrator(
inferenceResult.response.finishReason is FinishReason.ToolCall &&
toolRounds < MAX_TOOL_ROUNDS
) {
// stage_complete is a meta-tool: the LLM calls it to signal the stage's goal is met.
// Skip tool dispatch — the loop exits and normal validation/transition follows
// (emitToolArtifacts + verifyProduces run on the success path after the loop).
if (inferenceResult.response.toolCalls.any { it.function.name == STAGE_COMPLETE_TOOL }) {
break
}
val toolEntries = dispatchToolCalls(sessionId, stageId,
inferenceResult.response.toolCalls, stageConfig)
val fatalEntry = toolEntries.firstOrNull { it.content.startsWith("FATAL:") }
@@ -670,7 +678,14 @@ abstract class SessionOrchestrator(
parameters = tool.parametersSchema,
),
)
},
} + ToolDefinition(
function = ToolFunction(
name = STAGE_COMPLETE_TOOL,
description = "Call this tool when the stage's goal is fully met and no further tool calls are needed. " +
"The orchestrator will proceed to the next stage.",
parameters = JsonObject(emptyMap()),
),
),
)
// TODO: capture rendered prompt; currently contextpack.toString()