feat: surface ToolCallAssessedEvent to clients (plane-2 UX)
Final plane-2 step: the tool-call intent assessment was decided server-side but never reached the client — DomainEventMapper dropped ToolCallAssessedEvent through the else/null branch. - ServerMessage.ToolAssessed (tool.assessed): disposition as a plain String + AssessedIssueDto list; observations stay off the wire (internal replay facts, invariant #9). - DomainEventMapper maps the event 1:1 (no filtering — rendering decisions belong in the client). - Go TUI consumes tool.assessed, records an event entry only for non-PROCEED or issue-bearing assessments (noise control), and is added to IsEventBearing() so it buffers correctly during snapshot replay like its sibling tool.* events.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.correx.apps.server.bridge
|
||||
|
||||
import com.correx.apps.server.protocol.AssessedIssueDto
|
||||
import com.correx.apps.server.protocol.PauseReason
|
||||
import com.correx.apps.server.protocol.RiskSummaryDto
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
@@ -21,6 +22,7 @@ import com.correx.core.events.events.StoredEvent
|
||||
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.ToolCallAssessedEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.TransitionExecutedEvent
|
||||
import com.correx.core.events.events.WorkflowCompletedEvent
|
||||
@@ -160,6 +162,17 @@ suspend fun domainEventToServerMessage(
|
||||
sessionSequence = sessionSequence,
|
||||
)
|
||||
|
||||
is ToolCallAssessedEvent -> ServerMessage.ToolAssessed(
|
||||
sessionId = p.sessionId,
|
||||
stageId = p.stageId,
|
||||
toolName = p.toolName,
|
||||
disposition = p.disposition.name,
|
||||
issues = p.issues.map { AssessedIssueDto(it.code, it.message, it.severity) },
|
||||
occurredAt = event.metadata.timestamp.toEpochMilliseconds(),
|
||||
sequence = seq,
|
||||
sessionSequence = sessionSequence,
|
||||
)
|
||||
|
||||
is ApprovalRequestedEvent -> mapApprovalRequested(p, seq, sessionSequence)
|
||||
is ApprovalDecisionResolvedEvent -> ServerMessage.ApprovalResolved(
|
||||
// ApprovalDecisionResolvedEvent has no sessionId on its payload — read it from the event envelope
|
||||
|
||||
@@ -12,6 +12,13 @@ data class RiskSummaryDto(
|
||||
val rationale: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AssessedIssueDto(
|
||||
val code: String,
|
||||
val message: String,
|
||||
val severity: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProviderHealthDto(
|
||||
val providerId: String,
|
||||
|
||||
@@ -208,6 +208,19 @@ sealed interface ServerMessage {
|
||||
override val sessionSequence: Long,
|
||||
) : ServerMessage, SessionMessage
|
||||
|
||||
@Serializable
|
||||
@SerialName("tool.assessed")
|
||||
data class ToolAssessed(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val toolName: String,
|
||||
val disposition: String,
|
||||
val issues: List<AssessedIssueDto>,
|
||||
val occurredAt: Long,
|
||||
override val sequence: Long,
|
||||
override val sessionSequence: Long,
|
||||
) : ServerMessage, SessionMessage
|
||||
|
||||
// -- Approval --
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.correx.apps.server.bridge
|
||||
|
||||
import com.correx.apps.server.protocol.AssessedIssueDto
|
||||
import com.correx.apps.server.protocol.PauseReason
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
import com.correx.core.approvals.ApprovalOutcome
|
||||
@@ -20,6 +21,8 @@ import com.correx.core.events.events.StoredEvent
|
||||
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.AssessedIssue
|
||||
import com.correx.core.events.events.ToolCallAssessedEvent
|
||||
import com.correx.core.events.events.ToolInvocationRequestedEvent
|
||||
import com.correx.core.events.events.ToolReceipt
|
||||
import com.correx.core.events.events.ToolRequest
|
||||
@@ -355,6 +358,35 @@ class DomainEventMapperTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ToolCallAssessedEvent maps to ToolAssessed`(): Unit = runTest {
|
||||
val event = storedEvent(
|
||||
ToolCallAssessedEvent(
|
||||
invocationId = ToolInvocationId("inv-1"),
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
toolName = "file_write",
|
||||
issues = listOf(AssessedIssue(code = "PATH_ESCAPE", message = "escapes workspace", severity = "HIGH")),
|
||||
disposition = RiskAction.BLOCK,
|
||||
timestampMs = occurredAt,
|
||||
),
|
||||
)
|
||||
val result = domainEventToServerMessage(event, noopStore, sessionSequence = 0L)
|
||||
assertEquals(
|
||||
ServerMessage.ToolAssessed(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
toolName = "file_write",
|
||||
disposition = "BLOCK",
|
||||
issues = listOf(AssessedIssueDto("PATH_ESCAPE", "escapes workspace", "HIGH")),
|
||||
occurredAt = occurredAt,
|
||||
sequence = event.sequence,
|
||||
sessionSequence = 0L,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ApprovalRequestedEvent maps to ApprovalRequired with null riskSummary falls back to tier name`(): Unit = runTest {
|
||||
val requestId = ApprovalRequestId("req-1")
|
||||
|
||||
Reference in New Issue
Block a user