返回 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 if e.Err != nil && e.Diagnostic != nil && e.Diagnostic.Kind != provider.FailureKindCancelled {
30 rows = append(rows, providerFailureNotice(e))
31 }
32 case e.Err != nil:
33 row = providerFailureNotice(e)
34 default:
35 row = Message{}
36 }
37 if row.Role != "" {
38 rows = append(rows, row)
39 }
40 if e.ReadCompletion != nil {
41 rows = append(rows, readCompletionMessage(e.ReadCompletion))
42 }
43 if e.ProtocolRecovery != nil && e.Status != event.TurnInterrupted {
44 rows = append(rows, Message{Role: "notice", Code: "protocol_recovery", Level: "info", Pending: true,
45 Content: "The interrupted task can continue from valid context.", ProtocolRecovery: e.ProtocolRecovery})
46 }
47 for _, row := range rows {
48 present := false
49 for _, existing := range p.buffer.messages {
50 if existing.message.TurnID == e.TurnID && existing.message.Code == row.Code && row.Code != "" {
51 present = true
52 break
53 }
54 }
55 if present {
56 continue
57 }
58 row.RecordID = fmt.Sprintf("terminal:%s:%s", e.TurnID, row.Code)
59 row.TurnID, row.Source = e.TurnID, e.Source
60 p.buffer.messages = append(p.buffer.messages, &bufferedMessage{message: row})
61 }
62 }
63
64 func providerFailureNotice(e event.Event) Message {
65 return Message{Role: "notice", Code: event.NoticeCodeProviderRequestFailed, Level: "warn", Content: e.Err.Error(), Detail: e.Detail, Diagnostic: e.Diagnostic}
66 }
67
68 func (p *Projection) retireRecoveryNotices() {
69 for _, row := range p.buffer.messages {
70 if row.message.Role == "notice" && (row.message.ProtocolRecovery != nil || row.message.Readiness != nil) {
71 row.message.Pending = false
72 }
73 }
74 }
75
76 func readCompletionMessage(receipt *provider.ReadCompletion) Message {
77 parts := make([]string, 0, len(receipt.Reads))
78 for _, read := range receipt.Reads {
79 parts = append(parts, fmt.Sprintf("%s · %s · covered=%v", read.Path, read.Verdict, read.Covered))
80 }
81 return Message{Role: "notice", Code: "read_completion", Level: "info", Content: "Partial read coverage was accepted for this turn.",
82 Detail: strings.Join(parts, "\n"), ReadCompletion: receipt}
83 }
84
84 lines GO