mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-10 17:59:07 +00:00
feat: 支持类似claude code 会话历史的能力
This commit is contained in:
parent
edd6854f21
commit
daa7181ce1
6 changed files with 337 additions and 122 deletions
|
|
@ -103,7 +103,6 @@ type reviewOptions struct {
|
|||
outputFormat string
|
||||
concurrency int
|
||||
perFileTimeout int
|
||||
debug bool
|
||||
showHelp bool
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +120,6 @@ func parseReviewFlags(args []string) (reviewOptions, error) {
|
|||
a.StringVarP(&opts.outputFormat, "format", "f", "text", "output format: text or json")
|
||||
a.IntVar(&opts.concurrency, "concurrency", 8, "max concurrent file reviews")
|
||||
a.IntVar(&opts.perFileTimeout, "timeout", 10, "per-file timeout in minutes")
|
||||
a.BoolVar(&opts.debug, "debug", false, "enable debug mode (write session dump to temp/)")
|
||||
|
||||
if err := a.Parse(args); err != nil {
|
||||
return opts, fmt.Errorf("parse flags: %w", err)
|
||||
|
|
@ -182,7 +180,6 @@ Flags:`)
|
|||
fs.StringVar(&d.commit, "commit", "", "single commit hash or tag to review (vs its parent) (shorthand: -c)")
|
||||
fs.IntVar(&d.concurrency, "concurrency", 8, "max concurrent file reviews")
|
||||
fs.IntVar(&d.perFileTimeout, "timeout", 10, "per-file timeout in minutes")
|
||||
fs.BoolVar(&d.debug, "debug", false, "enable debug mode (write session dump to temp/)")
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ func runReview(args []string) error {
|
|||
CommentWorkerPool: agent.NewCommentWorkerPool(opts.concurrency),
|
||||
MaxConcurrency: opts.concurrency,
|
||||
PerFileTimeoutMinutes: opts.perFileTimeout,
|
||||
Debug: opts.debug,
|
||||
Model: model,
|
||||
})
|
||||
|
||||
|
|
|
|||
BIN
dist/opencodereview
vendored
BIN
dist/opencodereview
vendored
Binary file not shown.
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -80,11 +81,8 @@ type Args struct {
|
|||
// template phases (plan/memory_compression) don't specify one.
|
||||
Model string
|
||||
|
||||
// Debug enables debug mode which writes a session dump to temp/.
|
||||
Debug bool
|
||||
|
||||
// Session is an optional session history instance for collecting conversation records.
|
||||
// When nil, a default one is created automatically.
|
||||
// When nil, a default one is created automatically with git branch auto-detected from repoDir.
|
||||
Session *session.SessionHistory
|
||||
}
|
||||
|
||||
|
|
@ -159,9 +157,9 @@ func New(args Args) *Agent {
|
|||
args.CommentCollector = tool.NewCommentCollector()
|
||||
}
|
||||
if args.Session == nil {
|
||||
args.Session = session.New("", args.RepoDir)
|
||||
gitBranch := detectGitBranch(args.RepoDir)
|
||||
args.Session = session.New(args.RepoDir, gitBranch, args.Model)
|
||||
}
|
||||
args.Session.Debug = args.Debug
|
||||
return &Agent{
|
||||
args: args,
|
||||
session: args.Session,
|
||||
|
|
@ -797,3 +795,13 @@ func buildMessageXML(msgs []llm.Message) string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
// detectGitBranch returns the current git branch name for the given repo, or empty string on failure.
|
||||
func detectGitBranch(repoDir string) string {
|
||||
cmd := exec.Command("git", "-C", repoDir, "rev-parse", "--abbrev-ref", "HEAD")
|
||||
out, err := cmd.Output()
|
||||
if err != nil || len(out) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -18,20 +15,22 @@ import (
|
|||
type TaskType string
|
||||
|
||||
const (
|
||||
PlanTask TaskType = "plan_task"
|
||||
MainTask TaskType = "main_task"
|
||||
PlanTask TaskType = "plan_task"
|
||||
MainTask TaskType = "main_task"
|
||||
MemoryCompressionTask TaskType = "memory_compression_task"
|
||||
)
|
||||
|
||||
// SessionHistory is the top-level container for an entire CR run.
|
||||
// It is safe for concurrent use by multiple goroutines.
|
||||
type SessionHistory struct {
|
||||
mu sync.Mutex
|
||||
SessionID string
|
||||
RepoDir string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Debug bool
|
||||
mu sync.Mutex
|
||||
SessionID string
|
||||
RepoDir string
|
||||
GitBranch string
|
||||
Model string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
persist *jsonlWriter
|
||||
FileSessions map[string]*FileSession
|
||||
}
|
||||
|
||||
|
|
@ -40,17 +39,19 @@ type FileSession struct {
|
|||
mu sync.Mutex
|
||||
FilePath string
|
||||
TaskRecords map[TaskType][]*TaskRecord
|
||||
session *SessionHistory // back-reference for JSONL persistence
|
||||
}
|
||||
|
||||
// TaskRecord captures a single LLM request-response cycle within a file subtask.
|
||||
type TaskRecord struct {
|
||||
Type TaskType
|
||||
RequestNo int // sequential number within this task type
|
||||
RequestMessages []llm.Message // messages sent to LLM
|
||||
Response *ResponseRecord
|
||||
ToolResults []ToolResultRecord
|
||||
Duration time.Duration
|
||||
Error string
|
||||
Type TaskType
|
||||
RequestNo int // sequential number within this task type
|
||||
RequestMessages []llm.Message // messages sent to LLM
|
||||
Response *ResponseRecord
|
||||
ToolResults []ToolResultRecord
|
||||
Duration time.Duration
|
||||
Error string
|
||||
fileSession *FileSession // back-reference for JSONL persistence
|
||||
}
|
||||
|
||||
// TokenUsage holds estimated token usage for a single LLM request/response cycle.
|
||||
|
|
@ -77,14 +78,27 @@ type ToolResultRecord struct {
|
|||
Result string
|
||||
}
|
||||
|
||||
// New creates a new SessionHistory with the given session ID and repo directory.
|
||||
func New(sessionID, repoDir string) *SessionHistory {
|
||||
return &SessionHistory{
|
||||
SessionID: sessionID,
|
||||
RepoDir: repoDir,
|
||||
StartTime: time.Now(),
|
||||
// New creates a new SessionHistory with the given repo directory.
|
||||
func New(repoDir, gitBranch, model string) *SessionHistory {
|
||||
sessionID := generateUUID()
|
||||
sh := &SessionHistory{
|
||||
SessionID: sessionID,
|
||||
RepoDir: repoDir,
|
||||
GitBranch: gitBranch,
|
||||
Model: model,
|
||||
StartTime: time.Now(),
|
||||
FileSessions: make(map[string]*FileSession),
|
||||
}
|
||||
|
||||
p, err := newJSONLWriter(sessionID, repoDir, gitBranch, model)
|
||||
if err != nil {
|
||||
fmt.Printf("[ocr session] warning: failed to create session writer: %v\n", err)
|
||||
} else {
|
||||
sh.persist = p
|
||||
p.WriteSessionStart(sh.StartTime)
|
||||
}
|
||||
|
||||
return sh
|
||||
}
|
||||
|
||||
// GetOrCreateFileSession returns the FileSession for the given file path,
|
||||
|
|
@ -98,107 +112,34 @@ func (sh *SessionHistory) GetOrCreateFileSession(filePath string) *FileSession {
|
|||
fs = &FileSession{
|
||||
FilePath: filePath,
|
||||
TaskRecords: make(map[TaskType][]*TaskRecord),
|
||||
session: sh,
|
||||
}
|
||||
sh.FileSessions[filePath] = fs
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
// Finalize marks the session as complete and sets the end time.
|
||||
// Finalize marks the session as complete, sets the end time, and persists
|
||||
// the final summary record.
|
||||
func (sh *SessionHistory) Finalize() {
|
||||
sh.mu.Lock()
|
||||
sh.EndTime = time.Now()
|
||||
debug := sh.Debug
|
||||
sh.mu.Unlock()
|
||||
if debug {
|
||||
sh.writeDebugDump()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Debug Dump ---
|
||||
|
||||
type sessionSnapshot struct {
|
||||
SessionID string `json:"session_id"`
|
||||
RepoDir string `json:"repo_dir"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
FileSessions []*fileSessionSnapshot `json:"file_sessions"`
|
||||
}
|
||||
|
||||
type fileSessionSnapshot struct {
|
||||
FilePath string `json:"file_path"`
|
||||
TaskRecords map[string][]*taskRecordSnapshot `json:"task_records"`
|
||||
}
|
||||
|
||||
type taskRecordSnapshot struct {
|
||||
Type string `json:"type"`
|
||||
RequestNo int `json:"request_no"`
|
||||
RequestMessages any `json:"request_messages"`
|
||||
Response *ResponseRecord `json:"response"`
|
||||
ToolResults []ToolResultRecord `json:"tool_results"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (sh *SessionHistory) writeDebugDump() {
|
||||
sessionName := sh.SessionID
|
||||
if sessionName == "" {
|
||||
sessionName = fmt.Sprintf("unknown-%d", time.Now().Unix())
|
||||
}
|
||||
|
||||
sh.mu.Lock()
|
||||
snap := &sessionSnapshot{
|
||||
SessionID: sh.SessionID,
|
||||
RepoDir: sh.RepoDir,
|
||||
StartTime: sh.StartTime,
|
||||
EndTime: sh.EndTime,
|
||||
FileSessions: make([]*fileSessionSnapshot, 0, len(sh.FileSessions)),
|
||||
}
|
||||
for _, fs := range sh.FileSessions {
|
||||
fs.mu.Lock()
|
||||
fss := &fileSessionSnapshot{
|
||||
FilePath: fs.FilePath,
|
||||
TaskRecords: make(map[string][]*taskRecordSnapshot),
|
||||
}
|
||||
for ttype, records := range fs.TaskRecords {
|
||||
for _, rec := range records {
|
||||
fss.TaskRecords[string(ttype)] = append(fss.TaskRecords[string(ttype)], &taskRecordSnapshot{
|
||||
Type: string(rec.Type),
|
||||
RequestNo: rec.RequestNo,
|
||||
RequestMessages: rec.RequestMessages,
|
||||
Response: rec.Response,
|
||||
ToolResults: rec.ToolResults,
|
||||
Duration: rec.Duration,
|
||||
Error: rec.Error,
|
||||
})
|
||||
}
|
||||
}
|
||||
fs.mu.Unlock()
|
||||
snap.FileSessions = append(snap.FileSessions, fss)
|
||||
p := sh.persist
|
||||
duration := sh.EndTime.Sub(sh.StartTime)
|
||||
filesReviewed := make([]string, 0, len(sh.FileSessions))
|
||||
for fp := range sh.FileSessions {
|
||||
filesReviewed = append(filesReviewed, fp)
|
||||
}
|
||||
sh.mu.Unlock()
|
||||
|
||||
data, err := json.MarshalIndent(snap, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("[ocr debug] Failed to marshal session history: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
debugDir := filepath.Join(sh.RepoDir, "temp")
|
||||
if err := os.MkdirAll(debugDir, 0755); err != nil {
|
||||
fmt.Printf("[ocr debug] Failed to create debug dir %s: %v\n", debugDir, err)
|
||||
return
|
||||
}
|
||||
filename := filepath.Join(debugDir, fmt.Sprintf("ocr-session-%s.json", sessionName))
|
||||
if err := os.WriteFile(filename, data, 0644); err != nil {
|
||||
fmt.Printf("[ocr debug] Failed to write session dump to %s: %v\n", filename, err)
|
||||
} else {
|
||||
fmt.Printf("[ocr debug] Session history written to %s\n", filename)
|
||||
if p != nil {
|
||||
p.WriteSessionEnd(duration, filesReviewed)
|
||||
}
|
||||
}
|
||||
|
||||
// AppendTaskRecord adds a new task record to the file session for the given
|
||||
// file path and task type. It auto-assigns the RequestNo based on existing records.
|
||||
// file path and task type. It auto-assigns the RequestNo based on existing records
|
||||
// and writes an llm_request record to the JSONL stream.
|
||||
func (fs *FileSession) AppendTaskRecord(taskType TaskType, messages []llm.Message) *TaskRecord {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
|
|
@ -207,8 +148,14 @@ func (fs *FileSession) AppendTaskRecord(taskType TaskType, messages []llm.Messag
|
|||
Type: taskType,
|
||||
RequestNo: len(fs.TaskRecords[taskType]) + 1,
|
||||
RequestMessages: copyMessages(messages),
|
||||
fileSession: fs,
|
||||
}
|
||||
fs.TaskRecords[taskType] = append(fs.TaskRecords[taskType], rec)
|
||||
|
||||
if p := fs.session.persist; p != nil {
|
||||
p.WriteLLMRequest(fs.FilePath, taskType, rec.RequestNo, copyMessagesForJSON(messages))
|
||||
}
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
|
|
@ -227,8 +174,27 @@ func copyMessages(msgs []llm.Message) []llm.Message {
|
|||
return cp
|
||||
}
|
||||
|
||||
// copyMessagesForJSON produces a JSON-friendly slice for persistence.
|
||||
func copyMessagesForJSON(msgs []llm.Message) any {
|
||||
type msg struct {
|
||||
Role string `json:"role"`
|
||||
Content any `json:"content"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
}
|
||||
out := make([]msg, 0, len(msgs))
|
||||
for _, m := range msgs {
|
||||
out = append(out, msg{
|
||||
Role: m.Role,
|
||||
Content: m.Content,
|
||||
ToolCallID: m.ToolCallID,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SetResponse records the LLM response in the most recent TaskRecord of the given type.
|
||||
// It computes estimated token usage locally using tiktoken, independent of any API format.
|
||||
// It computes estimated token usage locally using tiktoken, independent of any API format,
|
||||
// and writes an llm_response record to the JSONL stream.
|
||||
func (tr *TaskRecord) SetResponse(resp *llm.ChatResponse, duration time.Duration) {
|
||||
if resp == nil || len(resp.Choices) == 0 {
|
||||
tr.Error = "empty response"
|
||||
|
|
@ -260,6 +226,20 @@ func (tr *TaskRecord) SetResponse(resp *llm.ChatResponse, duration time.Duration
|
|||
Usage: usage,
|
||||
}
|
||||
tr.Duration = duration
|
||||
|
||||
if fs := tr.fileSession; fs != nil {
|
||||
if p := fs.session.persist; p != nil {
|
||||
toolCallsJSON := make([]map[string]any, 0, len(choice.Message.ToolCalls))
|
||||
for _, tc := range choice.Message.ToolCalls {
|
||||
toolCallsJSON = append(toolCallsJSON, map[string]any{
|
||||
"id": tc.ID,
|
||||
"name": tc.Function.Name,
|
||||
"arguments": tc.Function.Arguments,
|
||||
})
|
||||
}
|
||||
p.WriteLLMResponse(fs.FilePath, tr.Type, content, toolCallsJSON, resp.Model, promptTokens, completionTokens, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetError records an error for this task record.
|
||||
|
|
@ -268,12 +248,18 @@ func (tr *TaskRecord) SetError(err error, duration time.Duration) {
|
|||
tr.Duration = duration
|
||||
}
|
||||
|
||||
// AddToolResult appends a tool call result to this task record.
|
||||
// AddToolResult appends a tool call result to this task record and writes a
|
||||
// tool_call record to the JSONL stream.
|
||||
func (tr *TaskRecord) AddToolResult(toolName, arguments, result string) {
|
||||
tr.ToolResults = append(tr.ToolResults, ToolResultRecord{
|
||||
ToolName: toolName,
|
||||
Arguments: arguments,
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
|
||||
if fs := tr.fileSession; fs != nil {
|
||||
if p := fs.session.persist; p != nil {
|
||||
p.WriteToolCall(fs.FilePath, tr.Type, toolName, arguments, result, true, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
225
internal/session/persist.go
Normal file
225
internal/session/persist.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// jsonlWriter streams session records to a JSONL file under
|
||||
// $HOME/.open-code-review/sessions/<encoded-repo-path>/<session-id>.jsonl.
|
||||
// It is safe for concurrent use by multiple goroutines.
|
||||
type jsonlWriter struct {
|
||||
mu sync.Mutex
|
||||
sessionID string
|
||||
repoDir string
|
||||
gitBranch string
|
||||
model string
|
||||
file *os.File
|
||||
writer *bufio.Writer
|
||||
lastUUID string // tracks chain of records via parentUuid
|
||||
}
|
||||
|
||||
// newJSONLWriter creates and opens a new JSONL writer for the given session.
|
||||
func newJSONLWriter(sessionID, repoDir, gitBranch, model string) (*jsonlWriter, error) {
|
||||
jw := &jsonlWriter{
|
||||
sessionID: sessionID,
|
||||
repoDir: repoDir,
|
||||
gitBranch: gitBranch,
|
||||
model: model,
|
||||
}
|
||||
if err := jw.open(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jw, nil
|
||||
}
|
||||
|
||||
func generateUUID() string {
|
||||
b := make([]byte, 16)
|
||||
_, err := io.ReadFull(rand.Reader, b)
|
||||
if err != nil {
|
||||
// Fallback — extremely unlikely but keeps things working without panics.
|
||||
return fmt.Sprintf("fallback-%d", time.Now().UnixNano())
|
||||
}
|
||||
b[6] = (b[6] & 0x0f) | 0x40 // version 4
|
||||
b[8] = (b[8] & 0x3f) | 0x80 // variant 1
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
|
||||
}
|
||||
|
||||
func encodeRepoPath(p string) string {
|
||||
p = strings.TrimPrefix(p, "/")
|
||||
return strings.ReplaceAll(p, "/", "-")
|
||||
}
|
||||
|
||||
func (jw *jsonlWriter) open() error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve home dir: %w", err)
|
||||
}
|
||||
|
||||
sessionDir := filepath.Join(home, ".open-code-review", "sessions", encodeRepoPath(jw.repoDir))
|
||||
if err := os.MkdirAll(sessionDir, 0755); err != nil {
|
||||
return fmt.Errorf("create session dir: %w", err)
|
||||
}
|
||||
|
||||
filename := filepath.Join(sessionDir, jw.sessionID+".jsonl")
|
||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open session file: %w", err)
|
||||
}
|
||||
|
||||
jw.file = f
|
||||
jw.writer = bufio.NewWriter(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (jw *jsonlWriter) writeRecordLocked(rec map[string]any) {
|
||||
data, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
fmt.Printf("[ocr session] failed to marshal record: %v\n", err)
|
||||
return
|
||||
}
|
||||
jw.writer.Write(data)
|
||||
jw.writer.WriteByte('\n')
|
||||
}
|
||||
|
||||
// WriteSessionStart writes the initial session_start record.
|
||||
func (jw *jsonlWriter) WriteSessionStart(startTime time.Time) string {
|
||||
uuid := generateUUID()
|
||||
rec := map[string]any{
|
||||
"uuid": uuid,
|
||||
"parentUuid": nil,
|
||||
"type": "session_start",
|
||||
"sessionId": jw.sessionID,
|
||||
"timestamp": startTime.UTC().Format(time.RFC3339),
|
||||
"cwd": jw.repoDir,
|
||||
"gitBranch": jw.gitBranch,
|
||||
"model": jw.model,
|
||||
}
|
||||
|
||||
jw.mu.Lock()
|
||||
defer jw.mu.Unlock()
|
||||
jw.writeRecordLocked(rec)
|
||||
jw.lastUUID = uuid
|
||||
return uuid
|
||||
}
|
||||
|
||||
// WriteLLMRequest writes a request entry with the resolved messages.
|
||||
func (jw *jsonlWriter) WriteLLMRequest(filePath string, taskType TaskType, requestNo int, messages any) string {
|
||||
uuid := generateUUID()
|
||||
|
||||
jw.mu.Lock()
|
||||
defer jw.mu.Unlock()
|
||||
rec := map[string]any{
|
||||
"uuid": uuid,
|
||||
"parentUuid": jw.lastUUID,
|
||||
"type": "llm_request",
|
||||
"sessionId": jw.sessionID,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"filePath": filePath,
|
||||
"taskType": string(taskType),
|
||||
"request_no": requestNo,
|
||||
"messages": messages,
|
||||
}
|
||||
jw.writeRecordLocked(rec)
|
||||
jw.lastUUID = uuid
|
||||
return uuid
|
||||
}
|
||||
|
||||
// WriteLLMResponse writes a response entry with model, content, tool calls, usage.
|
||||
func (jw *jsonlWriter) WriteLLMResponse(filePath string, taskType TaskType, content string, toolCalls []map[string]any, model string, promptTokens, completionTokens int, duration time.Duration) string {
|
||||
uuid := generateUUID()
|
||||
|
||||
jw.mu.Lock()
|
||||
defer jw.mu.Unlock()
|
||||
rec := map[string]any{
|
||||
"uuid": uuid,
|
||||
"parentUuid": jw.lastUUID,
|
||||
"type": "llm_response",
|
||||
"sessionId": jw.sessionID,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"filePath": filePath,
|
||||
"taskType": string(taskType),
|
||||
"model": model,
|
||||
"content": content,
|
||||
"tool_calls": toolCalls,
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
"usage": map[string]int{
|
||||
"prompt_tokens": promptTokens,
|
||||
"completion_tokens": completionTokens,
|
||||
},
|
||||
}
|
||||
jw.writeRecordLocked(rec)
|
||||
jw.lastUUID = uuid
|
||||
return uuid
|
||||
}
|
||||
|
||||
// WriteToolCall writes a tool call result entry.
|
||||
func (jw *jsonlWriter) WriteToolCall(filePath string, taskType TaskType, toolName, arguments, result string, ok bool, duration time.Duration) string {
|
||||
uuid := generateUUID()
|
||||
|
||||
jw.mu.Lock()
|
||||
defer jw.mu.Unlock()
|
||||
rec := map[string]any{
|
||||
"uuid": uuid,
|
||||
"parentUuid": jw.lastUUID,
|
||||
"type": "tool_call",
|
||||
"sessionId": jw.sessionID,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"filePath": filePath,
|
||||
"taskType": string(taskType),
|
||||
"tool_name": toolName,
|
||||
"arguments": arguments,
|
||||
"result": result,
|
||||
"ok": ok,
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
}
|
||||
jw.writeRecordLocked(rec)
|
||||
jw.lastUUID = uuid
|
||||
return uuid
|
||||
}
|
||||
|
||||
// WriteSessionEnd writes the final session_end summary record and closes the file.
|
||||
func (jw *jsonlWriter) WriteSessionEnd(duration time.Duration, filesReviewed []string) {
|
||||
uuid := generateUUID()
|
||||
|
||||
jw.mu.Lock()
|
||||
defer jw.mu.Unlock()
|
||||
rec := map[string]any{
|
||||
"uuid": uuid,
|
||||
"parentUuid": jw.lastUUID,
|
||||
"type": "session_end",
|
||||
"sessionId": jw.sessionID,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"files_reviewed": filesReviewed,
|
||||
"duration_seconds": duration.Seconds(),
|
||||
}
|
||||
jw.writeRecordLocked(rec)
|
||||
jw.lastUUID = uuid
|
||||
|
||||
if jw.writer != nil {
|
||||
jw.writer.Flush()
|
||||
}
|
||||
if jw.file != nil {
|
||||
jw.file.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (jw *jsonlWriter) flushAndClose() {
|
||||
jw.mu.Lock()
|
||||
defer jw.mu.Unlock()
|
||||
if jw.writer != nil {
|
||||
jw.writer.Flush()
|
||||
}
|
||||
if jw.file != nil {
|
||||
jw.file.Close()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue