fix: emit ToolExecutionCompleted/Failed/Rejected events after tool dispatch

ToolInvocationRequestedEvent was emitted but the corresponding outcome
events were never emitted, so the TUI permanently showed tools as
RUNNING. Now the orchestrator closes every tool invocation with the
appropriate event:
- ToolExecutionCompletedEvent (Success path)
- ToolExecutionFailedEvent (Failure path)
- ToolExecutionRejectedEvent (approval denied path)
This commit is contained in:
2026-05-26 21:17:07 +04:00
parent 766b91081a
commit 32d4cfa5dc
@@ -29,7 +29,11 @@ import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.RiskAssessedEvent
import com.correx.core.events.events.ToolExecutionCompletedEvent
import com.correx.core.events.events.ToolExecutionFailedEvent
import com.correx.core.events.events.ToolExecutionRejectedEvent
import com.correx.core.events.events.ToolInvocationRequestedEvent
import com.correx.core.events.events.ToolReceipt
import com.correx.core.events.events.ToolRequest
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.events.WorkflowCompletedEvent
@@ -371,6 +375,14 @@ abstract class SessionOrchestrator(
}
emitDecisionResolved(sessionId, domainRequest, decision)
if (!decision.isApproved) {
val rejectReason = decision.reason ?: "approval denied"
emit(sessionId, ToolExecutionRejectedEvent(
invocationId = invocationId,
sessionId = sessionId,
toolName = toolCall.function.name,
tier = tier,
reason = rejectReason,
))
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
val sourceId = toolCall.id ?: invocationId.value
val assistantEntry = ContextEntry(
@@ -381,14 +393,13 @@ abstract class SessionOrchestrator(
content = Json.encodeToString(ToolCallRequest.serializer(), toolCall),
tokenEstimate = toolCall.function.arguments.length / 4,
)
val reason = decision.reason ?: "approval denied"
val resultEntry = ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L2,
sourceType = "toolResult",
sourceId = sourceId,
content = "ERROR: $reason",
tokenEstimate = reason.length / 4,
content = "ERROR: $rejectReason",
tokenEstimate = rejectReason.length / 4,
)
return@flatMap listOf(assistantEntry, resultEntry)
}
@@ -421,6 +432,29 @@ abstract class SessionOrchestrator(
artifactContentCache["${sessionId.value}:${slot.name.value}"] = jsonContent
}
when (result) {
is ToolResult.Success -> emit(sessionId, ToolExecutionCompletedEvent(
invocationId = invocationId,
sessionId = sessionId,
toolName = toolCall.function.name,
receipt = ToolReceipt(
invocationId = invocationId,
toolName = toolCall.function.name,
exitCode = result.exitCode,
outputSummary = result.output.take(500),
durationMs = 0,
tier = tier,
timestamp = Clock.System.now(),
),
))
is ToolResult.Failure -> emit(sessionId, ToolExecutionFailedEvent(
invocationId = invocationId,
sessionId = sessionId,
toolName = toolCall.function.name,
reason = result.reason,
))
}
val sourceId = toolCall.id ?: invocationId.value
val assistantEntry = ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),