feat(server): REST /clarify route for headless clarification answers (#42)
Discovery-stage clarifications could only be answered over WebSocket
(ClientMessage.ClarificationResponse), so the curl-based headless QA
driver parked forever at discovery.
Add POST /sessions/{id}/clarify mirroring approveStageRoute. The server
resolves the live (stageId, requestId) from the session id via
SessionOrchestrator.pendingClarificationFor() — the newest still-live,
unanswered ClarificationRequestedEvent from the event log — so a curl
caller need not know the requestId. Empty answers = free-text skip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+18
@@ -9,6 +9,8 @@ import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.context.builder.ContextPackBuilder
|
||||
import com.correx.core.context.model.ContextPack
|
||||
import com.correx.core.events.events.ClarificationAnswer
|
||||
import com.correx.core.events.events.ClarificationAnsweredEvent
|
||||
import com.correx.core.events.events.ClarificationRequestedEvent
|
||||
import com.correx.core.events.events.InferenceCompletedEvent
|
||||
import com.correx.core.events.events.InferenceFailedEvent
|
||||
import com.correx.core.events.events.InferenceStartedEvent
|
||||
@@ -260,6 +262,22 @@ abstract class SessionOrchestrator(
|
||||
fun liveClarificationRequestIds(): Set<String> =
|
||||
pendingClarifications.keys.mapTo(mutableSetOf()) { it.value }
|
||||
|
||||
/**
|
||||
* The clarification a headless caller should answer for this session: the newest still-live,
|
||||
* unanswered request. Null if the session is not parked on a clarification. Lets a REST answer
|
||||
* route resolve the (stageId, requestId) server-side so a script need only supply answer values —
|
||||
* the WS path knows them because it received the [ClarificationRequestedEvent]; a curl caller does not.
|
||||
*/
|
||||
suspend fun pendingClarificationFor(sessionId: SessionId): ClarificationRequestedEvent? {
|
||||
val live = liveClarificationRequestIds()
|
||||
if (live.isEmpty()) return null
|
||||
val events = eventStore.read(sessionId)
|
||||
val answered = events.mapNotNull { it.payload as? ClarificationAnsweredEvent }
|
||||
.mapTo(mutableSetOf()) { it.requestId }
|
||||
return events.mapNotNull { it.payload as? ClarificationRequestedEvent }
|
||||
.lastOrNull { it.requestId.value in live && it.requestId !in answered }
|
||||
}
|
||||
|
||||
/** Public seam for out-of-band gates (e.g. freestyle plan review) that reuse the stage-approval flow. */
|
||||
suspend fun requestPlanApproval(sessionId: SessionId, preview: String): Boolean =
|
||||
requestStageApproval(sessionId, StageId("plan_review"), preview)
|
||||
|
||||
Reference in New Issue
Block a user