Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_stream.go

67 lines
1.8 KiB
Go

package app
import (
"errors"
"unicode/utf8"
"gorm.io/gorm"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
)
// Publish upstream deltas to the same durable, revisioned transcript used by
// Agent SSE/replay. Each flush re-reads state; never overwrite a scheduler's
// checkpoint with the snapshot from when the model request started.
func newCloudAgentStreamPublisher(s *Service, userID, taskID, kind string) *taskTextStreamPublisher {
p := newTaskTextStreamPublisher(s, userID, taskID)
written := 0
p.sink = func(delta string) error {
remaining := 32000 - written
if remaining <= 0 {
return nil
}
if len(delta) > remaining {
delta = delta[:remaining]
for !utf8.ValidString(delta) {
delta = delta[:len(delta)-1]
}
}
if delta == "" {
return nil
}
for attempt := 0; attempt < 3; attempt++ {
run, err := s.repo.CloudAgentForActiveTask(userID, taskID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
err = s.repo.MutateCloudAgent(userID, run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
state, err := cloudAgentDecode(current)
if err != nil {
return err
}
if state.ActiveTaskID != taskID || (current.Status != "running" && current.Status != "queued") {
return nil
}
messageID := taskID
if kind == "reasoning_delta" {
messageID += ":reasoning"
}
state.event(run.ID, kind, map[string]any{"messageId": messageID, "text": delta})
return cloudAgentSave(current, &state)
})
if errors.Is(err, repository.ErrCreationConflict) {
continue
}
if err == nil {
written += len(delta)
}
return err
}
return repository.ErrCreationConflict
}
return p
}