From cc502c7173199f85a3b936f94e0c43f42e358544 Mon Sep 17 00:00:00 2001 From: laily Date: Tue, 2 Jun 2026 14:28:26 +0800 Subject: [PATCH] feat: add transcript parser for last user/agent messages Co-authored-by: Cursor --- internal/transcript/testdata/long.jsonl | 2 + internal/transcript/testdata/sample.jsonl | 4 + internal/transcript/transcript.go | 93 +++++++++++++++++++++++ internal/transcript/transcript_test.go | 21 +++++ 4 files changed, 120 insertions(+) create mode 100644 internal/transcript/testdata/long.jsonl create mode 100644 internal/transcript/testdata/sample.jsonl create mode 100644 internal/transcript/transcript.go create mode 100644 internal/transcript/transcript_test.go diff --git a/internal/transcript/testdata/long.jsonl b/internal/transcript/testdata/long.jsonl new file mode 100644 index 0000000..da2d57a --- /dev/null +++ b/internal/transcript/testdata/long.jsonl @@ -0,0 +1,2 @@ +{"role": "user", "content": "q"} +{"role": "assistant", "content": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} diff --git a/internal/transcript/testdata/sample.jsonl b/internal/transcript/testdata/sample.jsonl new file mode 100644 index 0000000..6c601a4 --- /dev/null +++ b/internal/transcript/testdata/sample.jsonl @@ -0,0 +1,4 @@ +{"role":"user","content":"hello"} +{"role":"assistant","content":"world"} +{"role":"user","content":"second question"} +{"role":"assistant","content":"final answer"} diff --git a/internal/transcript/transcript.go b/internal/transcript/transcript.go new file mode 100644 index 0000000..b8e5c79 --- /dev/null +++ b/internal/transcript/transcript.go @@ -0,0 +1,93 @@ +package transcript + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "io" + "os" + "strings" + "unicode/utf8" +) + +const tailWindow = 256 * 1024 + +// LastMessages reads a JSONL transcript and returns the last user and assistant +// messages, each truncated to maxLen runes. +func LastMessages(path string, maxLen int) (lastUser, lastAgent string, err error) { + f, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return "", "", err + } + return "", "", err + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return "", "", err + } + size := info.Size() + + var r io.Reader = f + if size > tailWindow { + start := size - tailWindow + if _, err := f.Seek(start, io.SeekStart); err != nil { + return "", "", err + } + tail := make([]byte, tailWindow) + n, err := io.ReadFull(f, tail) + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + return "", "", err + } + tail = tail[:n] + if i := bytes.IndexByte(tail, '\n'); i >= 0 { + tail = tail[i+1:] + } + r = bytes.NewReader(tail) + } + + sc := bufio.NewScanner(r) + sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) + for sc.Scan() { + line := strings.TrimSpace(sc.Text()) + if line == "" { + continue + } + var row struct { + Role string `json:"role"` + Type string `json:"type"` + Content string `json:"content"` + } + if err := json.Unmarshal([]byte(line), &row); err != nil { + continue + } + role := row.Role + if role == "" { + role = row.Type + } + switch role { + case "user": + lastUser = row.Content + case "assistant": + lastAgent = row.Content + } + } + if err := sc.Err(); err != nil { + return "", "", err + } + + lastUser = truncateRunes(lastUser, maxLen) + lastAgent = truncateRunes(lastAgent, maxLen) + return lastUser, lastAgent, nil +} + +func truncateRunes(s string, maxLen int) string { + if maxLen <= 0 || utf8.RuneCountInString(s) <= maxLen { + return s + } + runes := []rune(s) + return string(runes[:maxLen]) +} diff --git a/internal/transcript/transcript_test.go b/internal/transcript/transcript_test.go new file mode 100644 index 0000000..277610c --- /dev/null +++ b/internal/transcript/transcript_test.go @@ -0,0 +1,21 @@ +package transcript + +import "testing" + +func TestLastMessages(t *testing.T) { + u, a, err := LastMessages("testdata/sample.jsonl", 2000) + if err != nil { + t.Fatal(err) + } + if u != "second question" || a != "final answer" { + t.Fatalf("got user=%q agent=%q", u, a) + } +} + +func TestTruncate2000(t *testing.T) { + u, a, err := LastMessages("testdata/long.jsonl", 2000) + if err != nil || len(a) != 2000 { + t.Fatalf("len=%d err=%v", len(a), err) + } + _ = u +}