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:
2026-05-31 16:49:00 +04:00
parent ff166ed311
commit 7fa1db8345
6 changed files with 112 additions and 26 deletions
+11
View File
@@ -134,6 +134,17 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
if msg.Diff != nil && *msg.Diff != "" {
m.appendRouter(msg.SessionID, RouterEntry{"tool", *msg.Diff})
}
case protocol.TypeToolAssessed:
if s := m.session(msg.SessionID); s != nil {
if msg.Disposition != "PROCEED" || len(msg.AssessedIssues) > 0 {
detail := msg.Disposition
if len(msg.AssessedIssues) > 0 {
detail += " [" + msg.AssessedIssues[0].Code + "]"
}
s.LastOutput = msg.ToolName + " assessed: " + detail
s.addEvent(msg.OccurredAt, "ToolAssessed", detail)
}
}
case protocol.TypeToolFailed:
if s := m.session(msg.SessionID); s != nil {
s.markTool(msg.ToolName, ToolFailed)