返回 DeepSeek-Reasonix
terminal.go
根目录 / internal / transcript / terminal.go
1 package transcript
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/event"
8 "reasonix/internal/provider"
9 )
10
11 // Terminal records belong to the authoritative snapshot, not only the live
12 // frontend. Advancing coverage without them would permanently hide recovery
13 // actions on reconnect, including after the WAL has been compacted.
14 func (p *Projection) applyTerminalNotices(e event.Event) {
15 var rows []Message
16 row := Message{Role: "notice", Level: "info"}
17 switch {
18 case e.Outcome == event.TurnOutcomeIncompleteRead:
19 row.Code, row.ReadPause = e.Outcome, e.ReadPause
20 case e.Outcome == event.TurnOutcomeFinalReadiness:
21 row.Code, row.Pending, row.Readiness = e.Outcome, true, e.Readiness
22 row.Content = "Task is not complete; continue the remaining work or checks."
23 case e.Outcome == event.TurnOutcomeRecoveryPaused:
24 row.Code, row.Content = e.Outcome, "Automatic recovery paused. You can continue the task."
25 case e.Outcome == event.TurnOutcomeCompletionUncertain:
26 row.Code, row.Content = e.Outcome, "The host could not confirm this turn is complete."
27 case e.Status == event.TurnInterrupted || e.Status == event.TurnRecoveryRequired:
28 row = interruptedNotice(nil)
29 case e.Err != nil:
30 row.Code, row.Level, row.Content, row.Detail = event.NoticeCodeProviderRequestFailed, "warn", e.Err.Error(), e.Detail
31 row.Diagnostic = e.Diagnostic
32 default:
33 row = Message{}
34 }
35 if row.Role != "" {
36 rows = append(rows, row)
37 }
38 if e.ReadCompletion != nil {
39 rows = append(rows, readCompletionMessage(e.ReadCompletion))
40 }
41 if e.ProtocolRecovery != nil && e.Status != event.TurnInterrupted {
42 rows = append(rows, Message{Role: "notice", Code: "protocol_recovery", Level: "info", Pending: true,
43 Content: "The interrupted task can continue from valid context.", ProtocolRecovery: e.ProtocolRecovery})
44 }
45 for _, row := range rows {
46 present := false
47 for _, existing := range p.buffer.messages {
48 if existing.message.TurnID == e.TurnID && existing.message.Code == row.Code && row.Code != "" {
49 present = true
50 break
51 }
52 }
53 if present {
54 continue
55 }
56 row.RecordID = fmt.Sprintf("terminal:%s:%s", e.TurnID, row.Code)
57 row.TurnID, row.Source = e.TurnID, e.Source
58 p.buffer.messages = append(p.buffer.messages, &bufferedMessage{message: row})
59 }
60 }
61
62 func (p *Projection) retireRecoveryNotices() {
63 for _, row := range p.buffer.messages {
64 if row.message.Role == "notice" && (row.message.ProtocolRecovery != nil || row.message.Readiness != nil) {
65 row.message.Pending = false
66 }
67 }
68 }
69
70 func readCompletionMessage(receipt *provider.ReadCompletion) Message {
71 parts := make([]string, 0, len(receipt.Reads))
72 for _, read := range receipt.Reads {
73 parts = append(parts, fmt.Sprintf("%s · %s · covered=%v", read.Path, read.Verdict, read.Covered))
74 }
75 return Message{Role: "notice", Code: "read_completion", Level: "info", Content: "Partial read coverage was accepted for this turn.",
76 Detail: strings.Join(parts, "\n"), ReadCompletion: receipt}
77 }
78
78 lines GO