13 // layoutFileName is the file of the view layout in the directory of an agent.
14 const layoutFileName = "layout.json"
16 // viewLayout splits the view of a turn request into user messages that only
17 // grow between batches. The ChatGPT backend writes a cache entry only at the
18 // end of the latest message of a request, and a later request reads it only at
19 // one of its own message ends. So the view goes out as the same messages as in
20 // the request before, while their lines give the same text, and the new lines
21 // follow as one new message. Then the end of each earlier view stays a message
22 // end, and a warm-up entry there stays readable when the view grows.
24 // The first message starts with "<chat>\n". The closing tag goes into the
25 // state message after the view, so that no view message changes when lines
26 // come. The layout keeps the line count and a hash of the text of each message
27 // in layout.json, so it outlives a restart.
28 type viewLayout struct {
30 Messages []layoutMessage `json:"messages"`
33 // layoutMessage is one view message: its number of lines and the hash of its
35 type layoutMessage struct {
36 Lines int `json:"lines"`
37 Hash string `json:"hash"`
40 // loadLayout reads the layout at path. A missing file gives an empty layout.
41 func loadLayout(path string) (*viewLayout, error) {
42 layout := &viewLayout{
46 data, err := os.ReadFile(path)
47 if errors.Is(err, os.ErrNotExist) {
52 return nil, fmt.Errorf("read view layout: %w", err)
55 err = json.Unmarshal(data, layout)
57 return nil, fmt.Errorf("decode view layout: %w", err)
63 // Split returns the view messages for lines and saves the layout. It keeps the
64 // earlier messages up to the first one whose text differs, and puts the rest
65 // of the lines into one new message.
66 func (l *viewLayout) Split(lines []string) ([]string, error) {
68 var kept []layoutMessage
70 for _, message := range l.Messages {
71 if start+message.Lines > len(lines) {
75 text := viewMessage(len(texts) == 0, lines[start:start+message.Lines])
76 if textHash(text) != message.Hash {
80 texts = append(texts, text)
81 kept = append(kept, message)
82 start += message.Lines
85 if start < len(lines) || len(texts) == 0 {
86 text := viewMessage(len(texts) == 0, lines[start:])
87 texts = append(texts, text)
88 kept = append(kept, layoutMessage{
89 Lines: len(lines) - start,
95 data, err := json.Marshal(l)
97 return nil, fmt.Errorf("encode view layout: %w", err)
100 err = writeFileAtomic(l.path, append(data, '\n'))
102 return nil, fmt.Errorf("save view layout: %w", err)
108 // viewMessage returns the text of a view message with lines. The first one
110 func viewMessage(first bool, lines []string) string {
111 text := strings.Join(lines, "")
113 text = "<chat>\n" + text
119 // textHash returns a short hash of text.
120 func textHash(text string) string {
121 sum := sha256.Sum256([]byte(text))
122 return hex.EncodeToString(sum[:16])