返回 DeepSeek-Reasonix
transcript_api.go
根目录 / internal / control / transcript_api.go
1 package control
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "reasonix/internal/session"
8 "time"
9
10 "reasonix/internal/transcript"
11 "reasonix/internal/turnevent"
12 )
13
14 var ErrTranscriptProjectionUnavailable = errors.New("transcript projection is unavailable")
15
16 type TranscriptReplayRequest struct {
17 Identity transcript.Identity `json:"identity"`
18 After uint64 `json:"after"`
19 }
20
21 type TranscriptReplay struct {
22 transcript.Boundary
23 turnevent.ReplayView
24 }
25
26 type TranscriptProjectionAPI interface {
27 TranscriptSnapshot(transcript.PageRequest) (transcript.Snapshot, error)
28 TranscriptContent(transcript.ContentRequest) (transcript.ContentChunk, error)
29 TranscriptReplay(TranscriptReplayRequest) (TranscriptReplay, error)
30 }
31
32 var _ TranscriptProjectionAPI = (*Controller)(nil)
33
34 type TranscriptFollowAPI interface {
35 TranscriptFollow(context.Context, transcript.FollowRequest) (TranscriptFollowResponse, error)
36 }
37
38 type TranscriptFollowResponse struct {
39 transcript.FollowResponse
40 History *session.HistoryWindowPage `json:"history,omitempty"`
41 }
42
43 func (c *Controller) TranscriptFollow(ctx context.Context, req transcript.FollowRequest) (TranscriptFollowResponse, error) {
44 service, runtime, exclusive := c.v3Binding()
45 if !exclusive || runtime == nil {
46 return TranscriptFollowResponse{}, ErrTranscriptProjectionUnavailable
47 }
48 view, err := runtime.FollowTranscript(ctx, req)
49 out := TranscriptFollowResponse{FollowResponse: view}
50 if err != nil || view.Snapshot == nil {
51 return out, err
52 }
53 // Make the frozen cut pageable even when accepted batches exceed the tail.
54 // The registered subscription queues concurrent commits during flush/read;
55 // no publisher lock is held.
56 cut := view.Snapshot.CoveredThroughSeq
57 if view.Snapshot.DurableSeq < cut {
58 receipt, flushErr := runtime.Session().Flush(ctx)
59 if flushErr != nil || receipt.DurableSequence < cut {
60 _, _ = runtime.Transcript().Follow(context.Background(), transcript.FollowRequest{Subscription: view.Subscription, Close: true})
61 if flushErr == nil {
62 flushErr = errors.New("transcript snapshot persistence is incomplete")
63 }
64 return out, flushErr
65 }
66 // Publish the new watermark in order, after already queued frames. Do
67 // not attach it to the older frozen view and then replay older watermarks.
68 runtime.Transcript().SetDurableSequence(receipt.DurableSequence)
69 }
70 var page session.HistoryWindowPage
71 for {
72 page, err = service.Query().ReadHistoryWindow(ctx, runtime.Ref(), session.HistoryWindowRequest{Anchor: "newest", Limit: 32, SnapshotSequence: &cut})
73 if err != nil || page.Status != "preparing" {
74 break
75 }
76 timer := time.NewTimer(10 * time.Millisecond)
77 select {
78 case <-ctx.Done():
79 timer.Stop()
80 err = ctx.Err()
81 case <-timer.C:
82 }
83 if err != nil {
84 break
85 }
86 }
87 if err != nil {
88 _, _ = runtime.Transcript().Follow(context.Background(), transcript.FollowRequest{Subscription: view.Subscription, Close: true})
89 }
90 out.History = &page
91 return out, err
92 }
93
94 // TranscriptOutlineAPI is an optional capability beside TranscriptProjectionAPI.
95 // It is deliberately separate so an existing controller implementation keeps
96 // compiling and a client can negotiate the outline independently of the body.
97 type TranscriptOutlineAPI interface {
98 TranscriptOutline(transcript.OutlineRequest) (transcript.OutlinePage, error)
99 }
100
101 var _ TranscriptOutlineAPI = (*Controller)(nil)
102
103 // SetTurnSubmissionID is called under the transport's admission boundary.
104 func (c *Controller) SetTurnSubmissionID(submissionID string) {
105 if ledger := c.turnEventLedger(); ledger != nil {
106 ledger.SetSubmissionID(submissionID)
107 }
108 }
109
110 // BindTranscriptRuntimeEpoch runs at the surface's idle runtime publication
111 // boundary. It takes only display/ledger leaf locks and invokes no callbacks.
112 func (c *Controller) BindTranscriptRuntimeEpoch(epoch string) {
113 c.turnEvents.commitMu.Lock()
114 defer c.turnEvents.commitMu.Unlock()
115 ledger := c.turnEventLedger()
116 if ledger == nil || ledger.ActiveTurnID() != "" {
117 return
118 }
119 ledger.SetRuntimeEpoch(epoch)
120 c.turnEvents.mu.RLock()
121 p := c.turnEvents.projection
122 c.turnEvents.mu.RUnlock()
123 if p != nil {
124 p.SetRuntimeEpoch(epoch)
125 }
126 }
127
128 func (c *Controller) transcriptProjection() (*transcript.Projection, error) {
129 if _, runtime, exclusive := c.v3Binding(); exclusive && runtime != nil {
130 return runtime.Transcript(), nil
131 }
132 c.turnEvents.mu.RLock()
133 defer c.turnEvents.mu.RUnlock()
134 if c.turnEvents.err != nil {
135 return nil, errors.Join(ErrTranscriptProjectionUnavailable, c.turnEvents.err)
136 }
137 if c.turnEvents.projectionErr != nil {
138 return nil, errors.Join(ErrTranscriptProjectionUnavailable, c.turnEvents.projectionErr)
139 }
140 if c.turnEvents.projection == nil {
141 return nil, ErrTranscriptProjectionUnavailable
142 }
143 return c.turnEvents.projection, nil
144 }
145
146 func (c *Controller) TranscriptSnapshot(req transcript.PageRequest) (transcript.Snapshot, error) {
147 p, err := c.transcriptProjection()
148 if err != nil {
149 return transcript.Snapshot{}, err
150 }
151 return p.Snapshot(req)
152 }
153
154 // TranscriptOutline pages the complete turn index of one snapshot. It reads the
155 // same projection the body pages do, so both describe one immutable cut.
156 func (c *Controller) TranscriptOutline(req transcript.OutlineRequest) (transcript.OutlinePage, error) {
157 p, err := c.transcriptProjection()
158 if err != nil {
159 return transcript.OutlinePage{}, err
160 }
161 return p.Outline(req)
162 }
163
164 func (c *Controller) TranscriptContent(req transcript.ContentRequest) (transcript.ContentChunk, error) {
165 p, err := c.transcriptProjection()
166 if err != nil {
167 return transcript.ContentChunk{}, err
168 }
169 return p.Content(req)
170 }
171
172 func (c *Controller) TranscriptReplay(req TranscriptReplayRequest) (TranscriptReplay, error) {
173 replay, err := c.transcriptReplay(req)
174 if err != nil {
175 return replay, err
176 }
177 // The ledger's soft budget permits an oversized first event for progress.
178 // Modern clients can obtain that data from the bounded snapshot/content
179 // protocol instead. Never acknowledge a suffix the client cannot receive.
180 encoded, err := json.Marshal(replay)
181 if err != nil {
182 return TranscriptReplay{}, err
183 }
184 if len(encoded)+1 > transcript.MaxResponseBytes {
185 replay.Events = []turnevent.Envelope{}
186 replay.ResetRequired, replay.HasMore = true, false
187 replay.NextAfterSequence = req.After
188 }
189 return replay, nil
190 }
191
192 func (c *Controller) transcriptReplay(req TranscriptReplayRequest) (TranscriptReplay, error) {
193 if _, runtime, exclusive := c.v3Binding(); exclusive && runtime != nil {
194 return TranscriptReplay{}, errors.New("transcript v2 requires Follow; legacy replay is unavailable")
195 }
196 c.turnEvents.commitMu.Lock()
197 defer c.turnEvents.commitMu.Unlock()
198 p, err := c.transcriptProjection()
199 if err != nil {
200 return TranscriptReplay{}, err
201 }
202 boundary := p.Boundary()
203 if boundary.Identity != req.Identity {
204 if boundary.Identity.SessionID != req.Identity.SessionID {
205 return TranscriptReplay{}, errors.New("transcript replay session mismatch")
206 }
207 return TranscriptReplay{Boundary: boundary, ReplayView: turnevent.ReplayView{Events: []turnevent.Envelope{}, ResetRequired: true, LatestSequence: boundary.CoveredThroughSeq}}, nil
208 }
209 view, err := c.TurnEventReplay(req.After)
210 return TranscriptReplay{Boundary: boundary, ReplayView: view}, err
211 }
212
212 lines GO