fix(kernel+infra): tool approval gate and executor cleanup

- SessionOrchestrator: wire T2/T3/T4 approval gate before tool execution;
  emit OrchestrationPausedEvent/ApprovalRequestedEvent, await decision,
  return rejection as ERROR context entry so LLM sees the denial;
  propagate tool ERROR entries as StageExecutionResult.Failure
- SandboxedToolExecutor: remove dead code and simplify
- InfrastructureModule: minor wiring cleanup
- LlamaCppInferenceProvider / build.gradle: related build fixes
This commit is contained in:
2026-05-19 00:05:08 +04:00
parent f32d400138
commit c0efa0ef03
5 changed files with 86 additions and 90 deletions
@@ -238,6 +238,10 @@ abstract class SessionOrchestrator(
toolRounds < MAX_TOOL_ROUNDS
) {
val toolEntries = dispatchToolCalls(sessionId, stageId, inferenceResult.response.toolCalls)
val firstError = toolEntries.firstOrNull { it.content.startsWith("ERROR:") }
if (firstError != null) {
return StageExecutionResult.Failure(firstError.content.removePrefix("ERROR: "), retryable = true)
}
val allEntries = currentContext.layers.values.flatten() + toolEntries
currentContext = contextPackBuilder.build(
id = ContextPackId(UUID.randomUUID().toString()),
@@ -297,6 +301,63 @@ abstract class SessionOrchestrator(
request = request,
),
)
val requiresApproval = when (tier) {
Tier.T0, Tier.T1 -> false
Tier.T2, Tier.T3, Tier.T4 -> true
}
if (requiresApproval) {
val requestId = ApprovalRequestId(UUID.randomUUID().toString())
val domainRequest = DomainApprovalRequest(
id = requestId,
tier = tier,
validationReportId = ValidationReportId(UUID.randomUUID().toString()),
riskSummaryId = null,
timestamp = Clock.System.now(),
)
emit(sessionId, OrchestrationPausedEvent(sessionId, stageId, "APPROVAL_PENDING"))
emit(
sessionId,
ApprovalRequestedEvent(
requestId = requestId,
tier = tier,
validationReportId = domainRequest.validationReportId,
riskSummaryId = null,
sessionId = sessionId,
stageId = stageId,
projectId = null,
),
)
val deferred = CompletableDeferred<ApprovalDecision>()
pendingApprovals[requestId] = deferred
val decision = try {
deferred.await()
} finally {
pendingApprovals.remove(requestId)
}
emitDecisionResolved(sessionId, domainRequest, decision)
if (!decision.isApproved) {
val sourceId = toolCall.id ?: invocationId.value
val assistantEntry = ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L2,
sourceType = "assistantToolCall",
sourceId = sourceId,
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,
)
return@flatMap listOf(assistantEntry, resultEntry)
}
emit(sessionId, OrchestrationResumedEvent(sessionId, stageId))
}
val result = executor.execute(request)
val sourceId = toolCall.id ?: invocationId.value
val assistantEntry = ContextEntry(