persist usage/cost data across session restarts

This commit is contained in:
ollie 2026-07-26 19:01:19 +02:00
parent 95557b35ab
commit d903980256
3 changed files with 42 additions and 17 deletions

View File

@ -411,10 +411,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
s.emit(infoEvent(fmt.Sprintf("error: %v", err)))
return
}
s.session = RestoreSession(ps.Messages)
if ps.TaskState != nil {
s.session.TaskState = ps.TaskState
}
s.session = RestoreSession(&ps)
s.emit(infoEvent(fmt.Sprintf("resumed session %s (%d messages)", name, len(ps.Messages))))
},

View File

@ -2177,7 +2177,7 @@ func TestRestoreSession_RoundTrip(t *testing.T) {
t.Errorf("ps.ID=%q ps.Agent=%q", ps.ID, ps.Agent)
}
restored := RestoreSession(ps.Messages)
restored := RestoreSession(&ps)
if restored.goal != "first user message" {
t.Errorf("goal = %q; want %q", restored.goal, "first user message")
}
@ -2192,7 +2192,7 @@ func TestRestoreSession_GoalFromFirstUserMessage(t *testing.T) {
{Role: "user", Content: "the real goal"},
{Role: "user", Content: "second user msg"},
}
s := RestoreSession(msgs)
s := RestoreSession(&PersistedSession{Messages: msgs})
if s.goal != "the real goal" {
t.Errorf("goal = %q; want %q", s.goal, "the real goal")
}

View File

@ -43,6 +43,15 @@ type PersistedSession struct {
Remote string `json:"remote,omitempty"`
Messages []backend.Message `json:"messages"`
TaskState *TaskState `json:"taskState,omitempty"`
// Usage and cost tracking, persisted across restarts.
TotalInputTokens int `json:"totalInputTokens,omitempty"`
TotalCachedInputTokens int `json:"totalCachedInputTokens,omitempty"`
TotalCacheCreationTokens int `json:"totalCacheCreationTokens,omitempty"`
TotalOutputTokens int `json:"totalOutputTokens,omitempty"`
TotalRequests int `json:"totalRequests,omitempty"`
Estimated bool `json:"estimated,omitempty"`
LastTurnCostUSD float64 `json:"lastTurnCostUSD,omitempty"`
SessionCostUSD float64 `json:"sessionCostUSD,omitempty"`
}
// TaskState is a compact structured overlay that summarizes the agent's
@ -92,14 +101,22 @@ func (s *Session) saveTo(path, id, agentName, cwd string) error {
// saveToFull writes session state including backend/model info.
func (s *Session) saveToFull(path, id, agentName, backendName, modelName, cwd, remote string) error {
ps := PersistedSession{
ID: id,
Agent: agentName,
Backend: backendName,
Model: modelName,
CWD: cwd,
Remote: remote,
Messages: s.messages,
TaskState: s.TaskState,
ID: id,
Agent: agentName,
Backend: backendName,
Model: modelName,
CWD: cwd,
Remote: remote,
Messages: s.messages,
TaskState: s.TaskState,
TotalInputTokens: s.TotalInputTokens,
TotalCachedInputTokens: s.TotalCachedInputTokens,
TotalCacheCreationTokens: s.TotalCacheCreationTokens,
TotalOutputTokens: s.TotalOutputTokens,
TotalRequests: s.TotalRequests,
Estimated: s.Estimated,
LastTurnCostUSD: s.LastTurnCostUSD,
SessionCostUSD: s.SessionCostUSD,
}
data, err := json.Marshal(ps)
if err != nil {
@ -122,9 +139,20 @@ func LoadPersistedSession(path string) (*PersistedSession, error) {
}
// RestoreSession reconstructs a Session from a persisted message list.
func RestoreSession(messages []backend.Message) *Session {
s := &Session{messages: messages}
for _, m := range messages {
func RestoreSession(ps *PersistedSession) *Session {
s := &Session{
messages: ps.Messages,
TaskState: ps.TaskState,
TotalInputTokens: ps.TotalInputTokens,
TotalCachedInputTokens: ps.TotalCachedInputTokens,
TotalCacheCreationTokens: ps.TotalCacheCreationTokens,
TotalOutputTokens: ps.TotalOutputTokens,
TotalRequests: ps.TotalRequests,
Estimated: ps.Estimated,
LastTurnCostUSD: ps.LastTurnCostUSD,
SessionCostUSD: ps.SessionCostUSD,
}
for _, m := range ps.Messages {
if m.Role == "user" {
s.goal = m.Content
break