on main help sign in
Improve summary to keep context (#159)
3e42475Kujtim Hoxha
internal/db/db.go+1−1
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.27.0
3+// sqlc v1.29.0
4
4
5package db
5package db
6
6
internal/db/files.sql.go+1−1
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.27.0
3+// sqlc v1.29.0
4// source: files.sql
4// source: files.sql
5
5
6package db
6package db
internal/db/messages.sql.go+1−1
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.27.0
3+// sqlc v1.29.0
4// source: messages.sql
4// source: messages.sql
5
5
6package db
6package db
internal/db/migrations/20250515105448_add_summary_message_id.sqlnew file+9
@@ -0,0 +1,9 @@
1+-- +goose Up
2+-- +goose StatementBegin
3+ALTER TABLE sessions ADD COLUMN summary_message_id TEXT;
4+-- +goose StatementEnd
5+
6+-- +goose Down
7+-- +goose StatementBegin
8+ALTER TABLE sessions DROP COLUMN summary_message_id;
9+-- +goose StatementEnd
internal/db/models.go+2−1
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.27.0
3+// sqlc v1.29.0
4
4
5package db
5package db
6
6
@@ -39,4 +39,5 @@ type Session struct {
39 Cost float64 `json:"cost"`
39 Cost float64 `json:"cost"`
40 UpdatedAt int64 `json:"updated_at"`
40 UpdatedAt int64 `json:"updated_at"`
41 CreatedAt int64 `json:"created_at"`
41 CreatedAt int64 `json:"created_at"`
42+ SummaryMessageID sql.NullString `json:"summary_message_id"`
42}
43}
internal/db/querier.go+1−1
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.27.0
3+// sqlc v1.29.0
4
4
5package db
5package db
6
6
internal/db/sessions.sql.go+19−10
@@ -1,6 +1,6 @@
1// Code generated by sqlc. DO NOT EDIT.
1// Code generated by sqlc. DO NOT EDIT.
2// versions:
2// versions:
3−// sqlc v1.27.0
3+// sqlc v1.29.0
4// source: sessions.sql
4// source: sessions.sql
5
5
6package db
6package db
@@ -19,6 +19,7 @@ INSERT INTO sessions (
19 prompt_tokens,
19 prompt_tokens,
20 completion_tokens,
20 completion_tokens,
21 cost,
21 cost,
22+ summary_message_id,
22 updated_at,
23 updated_at,
23 created_at
24 created_at
24) VALUES (
25) VALUES (
@@ -29,9 +30,10 @@ INSERT INTO sessions (
29 ?,
30 ?,
30 ?,
31 ?,
31 ?,
32 ?,
33+ null,
32 strftime('%s', 'now'),
34 strftime('%s', 'now'),
33 strftime('%s', 'now')
35 strftime('%s', 'now')
34−) RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
36+) RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id
35`
37`
36
38
37type CreateSessionParams struct {
39type CreateSessionParams struct {
@@ -65,6 +67,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S
65 &i.Cost,
67 &i.Cost,
66 &i.UpdatedAt,
68 &i.UpdatedAt,
67 &i.CreatedAt,
69 &i.CreatedAt,
70+ &i.SummaryMessageID,
68 )
71 )
69 return i, err
72 return i, err
70}
73}
@@ -80,7 +83,7 @@ func (q *Queries) DeleteSession(ctx context.Context, id string) error {
80}
83}
81
84
82const getSessionByID = `-- name: GetSessionByID :one
85const getSessionByID = `-- name: GetSessionByID :one
83−SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
86+SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id
84FROM sessions
87FROM sessions
85WHERE id = ? LIMIT 1
88WHERE id = ? LIMIT 1
86`
89`
@@ -98,12 +101,13 @@ func (q *Queries) GetSessionByID(ctx context.Context, id string) (Session, error
98 &i.Cost,
101 &i.Cost,
99 &i.UpdatedAt,
102 &i.UpdatedAt,
100 &i.CreatedAt,
103 &i.CreatedAt,
104+ &i.SummaryMessageID,
101 )
105 )
102 return i, err
106 return i, err
103}
107}
104
108
105const listSessions = `-- name: ListSessions :many
109const listSessions = `-- name: ListSessions :many
106−SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
110+SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id
107FROM sessions
111FROM sessions
108WHERE parent_session_id is NULL
112WHERE parent_session_id is NULL
109ORDER BY created_at DESC
113ORDER BY created_at DESC
@@ -128,6 +132,7 @@ func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) {
128 &i.Cost,
132 &i.Cost,
129 &i.UpdatedAt,
133 &i.UpdatedAt,
130 &i.CreatedAt,
134 &i.CreatedAt,
135+ &i.SummaryMessageID,
131 ); err != nil {
136 ); err != nil {
132 return nil, err
137 return nil, err
133 }
138 }
@@ -148,17 +153,19 @@ SET
148 title = ?,
153 title = ?,
149 prompt_tokens = ?,
154 prompt_tokens = ?,
150 completion_tokens = ?,
155 completion_tokens = ?,
156+ summary_message_id = ?,
151 cost = ?
157 cost = ?
152WHERE id = ?
158WHERE id = ?
153−RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at
159+RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id
154`
160`
155
161
156type UpdateSessionParams struct {
162type UpdateSessionParams struct {
157− Title string `json:"title"`
163+ Title string `json:"title"`
158− PromptTokens int64 `json:"prompt_tokens"`
164+ PromptTokens int64 `json:"prompt_tokens"`
159− CompletionTokens int64 `json:"completion_tokens"`
165+ CompletionTokens int64 `json:"completion_tokens"`
166+ SummaryMessageID sql.NullString `json:"summary_message_id"`
160− Cost float64 `json:"cost"`
167+ Cost float64 `json:"cost"`
161− ID string `json:"id"`
168+ ID string `json:"id"`
162}
169}
163
170
164func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error) {
171func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error) {
@@ -166,6 +173,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
166 arg.Title,
173 arg.Title,
167 arg.PromptTokens,
174 arg.PromptTokens,
168 arg.CompletionTokens,
175 arg.CompletionTokens,
176+ arg.SummaryMessageID,
169 arg.Cost,
177 arg.Cost,
170 arg.ID,
178 arg.ID,
171 )
179 )
@@ -180,6 +188,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S
180 &i.Cost,
188 &i.Cost,
181 &i.UpdatedAt,
189 &i.UpdatedAt,
182 &i.CreatedAt,
190 &i.CreatedAt,
191+ &i.SummaryMessageID,
183 )
192 )
184 return i, err
193 return i, err
185}
194}
internal/db/sql/sessions.sql+3
@@ -7,6 +7,7 @@ INSERT INTO sessions (
7 prompt_tokens,
7 prompt_tokens,
8 completion_tokens,
8 completion_tokens,
9 cost,
9 cost,
10+ summary_message_id,
10 updated_at,
11 updated_at,
11 created_at
12 created_at
12) VALUES (
13) VALUES (
@@ -17,6 +18,7 @@ INSERT INTO sessions (
17 ?,
18 ?,
18 ?,
19 ?,
19 ?,
20 ?,
21+ null,
20 strftime('%s', 'now'),
22 strftime('%s', 'now'),
21 strftime('%s', 'now')
23 strftime('%s', 'now')
22) RETURNING *;
24) RETURNING *;
@@ -38,6 +40,7 @@ SET
38 title = ?,
40 title = ?,
39 prompt_tokens = ?,
41 prompt_tokens = ?,
40 completion_tokens = ?,
42 completion_tokens = ?,
43+ summary_message_id = ?,
41 cost = ?
44 cost = ?
42WHERE id = ?
45WHERE id = ?
43RETURNING *;
46RETURNING *;
internal/llm/agent/agent.go+46−14
@@ -6,6 +6,7 @@ import (
6 "fmt"
6 "fmt"
7 "strings"
7 "strings"
8 "sync"
8 "sync"
9+ "time"
9
10
10 "github.com/opencode-ai/opencode/internal/config"
11 "github.com/opencode-ai/opencode/internal/config"
11 "github.com/opencode-ai/opencode/internal/llm/models"
12 "github.com/opencode-ai/opencode/internal/llm/models"
@@ -245,6 +246,23 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
245 }
246 }
246 }()
247 }()
247 }
248 }
249+ session, err := a.sessions.Get(ctx, sessionID)
250+ if err != nil {
251+ return a.err(fmt.Errorf("failed to get session: %w", err))
252+ }
253+ if session.SummaryMessageID != "" {
254+ summaryMsgInex := -1
255+ for i, msg := range msgs {
256+ if msg.ID == session.SummaryMessageID {
257+ summaryMsgInex = i
258+ break
259+ }
260+ }
261+ if summaryMsgInex != -1 {
262+ msgs = msgs[summaryMsgInex:]
263+ msgs[0].Role = message.User
264+ }
265+ }
248
266
249 userMsg, err := a.createUserMessage(ctx, sessionID, content, attachmentParts)
267 userMsg, err := a.createUserMessage(ctx, sessionID, content, attachmentParts)
250 if err != nil {
268 if err != nil {
@@ -614,37 +632,51 @@ func (a *agent) Summarize(ctx context.Context, sessionID string) error {
614 a.Publish(pubsub.CreatedEvent, event)
632 a.Publish(pubsub.CreatedEvent, event)
615 return
633 return
616 }
634 }
617− // Create a new session with the summary
635+ // Create a message in the new session with the summary
618− newSession, err := a.sessions.Create(summarizeCtx, oldSession.Title+" - Continuation")
636+ msg, err := a.messages.Create(summarizeCtx, oldSession.ID, message.CreateMessageParams{
637+ Role: message.Assistant,
638+ Parts: []message.ContentPart{
639+ message.TextContent{Text: summary},
640+ message.Finish{
641+ Reason: message.FinishReasonEndTurn,
642+ Time: time.Now().Unix(),
643+ },
644+ },
645+ Model: a.summarizeProvider.Model().ID,
646+ })
619 if err != nil {
647 if err != nil {
620 event = AgentEvent{
648 event = AgentEvent{
621 Type: AgentEventTypeError,
649 Type: AgentEventTypeError,
622− Error: fmt.Errorf("failed to create new session: %w", err),
650+ Error: fmt.Errorf("failed to create summary message: %w", err),
623 Done: true,
651 Done: true,
624 }
652 }
653+
625 a.Publish(pubsub.CreatedEvent, event)
654 a.Publish(pubsub.CreatedEvent, event)
626 return
655 return
627 }
656 }
628−
657+ oldSession.SummaryMessageID = msg.ID
629− // Create a message in the new session with the summary
658+ oldSession.CompletionTokens = response.Usage.OutputTokens
630− _, err = a.messages.Create(summarizeCtx, newSession.ID, message.CreateMessageParams{
659+ oldSession.PromptTokens = 0
631− Role: message.Assistant,
660+ model := a.summarizeProvider.Model()
632− Parts: []message.ContentPart{message.TextContent{Text: summary}},
661+ usage := response.Usage
633− Model: a.summarizeProvider.Model().ID,
662+ cost := model.CostPer1MInCached/1e6*float64(usage.CacheCreationTokens) +
634− })
663+ model.CostPer1MOutCached/1e6*float64(usage.CacheReadTokens) +
664+ model.CostPer1MIn/1e6*float64(usage.InputTokens) +
665+ model.CostPer1MOut/1e6*float64(usage.OutputTokens)
666+ oldSession.Cost += cost
667+ _, err = a.sessions.Save(summarizeCtx, oldSession)
635 if err != nil {
668 if err != nil {
636 event = AgentEvent{
669 event = AgentEvent{
637 Type: AgentEventTypeError,
670 Type: AgentEventTypeError,
638− Error: fmt.Errorf("failed to create summary message: %w", err),
671+ Error: fmt.Errorf("failed to save session: %w", err),
639 Done: true,
672 Done: true,
640 }
673 }
641−
642 a.Publish(pubsub.CreatedEvent, event)
674 a.Publish(pubsub.CreatedEvent, event)
643− return
644 }
675 }
676+
645 event = AgentEvent{
677 event = AgentEvent{
646 Type: AgentEventTypeSummarize,
678 Type: AgentEventTypeSummarize,
647− SessionID: newSession.ID,
679+ SessionID: oldSession.ID,
648 Progress: "Summary complete",
680 Progress: "Summary complete",
649 Done: true,
681 Done: true,
650 }
682 }
internal/session/session.go+7−1
@@ -16,6 +16,7 @@ type Session struct {
16 MessageCount int64
16 MessageCount int64
17 PromptTokens int64
17 PromptTokens int64
18 CompletionTokens int64
18 CompletionTokens int64
19+ SummaryMessageID string
19 Cost float64
20 Cost float64
20 CreatedAt int64
21 CreatedAt int64
21 UpdatedAt int64
22 UpdatedAt int64
@@ -105,7 +106,11 @@ func (s *service) Save(ctx context.Context, session Session) (Session, error) {
105 Title: session.Title,
106 Title: session.Title,
106 PromptTokens: session.PromptTokens,
107 PromptTokens: session.PromptTokens,
107 CompletionTokens: session.CompletionTokens,
108 CompletionTokens: session.CompletionTokens,
109+ SummaryMessageID: sql.NullString{
110+ String: session.SummaryMessageID,
111+ Valid: session.SummaryMessageID != "",
112+ },
108− Cost: session.Cost,
113+ Cost: session.Cost,
109 })
114 })
110 if err != nil {
115 if err != nil {
111 return Session{}, err
116 return Session{}, err
@@ -135,6 +140,7 @@ func (s service) fromDBItem(item db.Session) Session {
135 MessageCount: item.MessageCount,
140 MessageCount: item.MessageCount,
136 PromptTokens: item.PromptTokens,
141 PromptTokens: item.PromptTokens,
137 CompletionTokens: item.CompletionTokens,
142 CompletionTokens: item.CompletionTokens,
143+ SummaryMessageID: item.SummaryMessageID.String,
138 Cost: item.Cost,
144 Cost: item.Cost,
139 CreatedAt: item.CreatedAt,
145 CreatedAt: item.CreatedAt,
140 UpdatedAt: item.UpdatedAt,
146 UpdatedAt: item.UpdatedAt,
internal/tui/components/chat/list.go+11
@@ -99,6 +99,14 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
99 case renderFinishedMsg:
99 case renderFinishedMsg:
100 m.rendering = false
100 m.rendering = false
101 m.viewport.GotoBottom()
101 m.viewport.GotoBottom()
102+ case pubsub.Event[session.Session]:
103+ if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.session.ID {
104+ m.session = msg.Payload
105+ if m.session.SummaryMessageID == m.currentMsgID {
106+ delete(m.cachedContent, m.currentMsgID)
107+ m.renderView()
108+ }
109+ }
102 case pubsub.Event[message.Message]:
110 case pubsub.Event[message.Message]:
103 needsRerender := false
111 needsRerender := false
104 if msg.Type == pubsub.CreatedEvent {
112 if msg.Type == pubsub.CreatedEvent {
@@ -208,12 +216,15 @@ func (m *messagesCmp) renderView() {
208 m.uiMessages = append(m.uiMessages, cache.content...)
216 m.uiMessages = append(m.uiMessages, cache.content...)
209 continue
217 continue
210 }
218 }
219+ isSummary := m.session.SummaryMessageID == msg.ID
220+
211 assistantMessages := renderAssistantMessage(
221 assistantMessages := renderAssistantMessage(
212 msg,
222 msg,
213 inx,
223 inx,
214 m.messages,
224 m.messages,
215 m.app.Messages,
225 m.app.Messages,
216 m.currentMsgID,
226 m.currentMsgID,
227+ isSummary,
217 m.width,
228 m.width,
218 pos,
229 pos,
219 )
230 )
internal/tui/components/chat/message.go+4
@@ -120,6 +120,7 @@ func renderAssistantMessage(
120 allMessages []message.Message, // we need this to get tool results and the user message
120 allMessages []message.Message, // we need this to get tool results and the user message
121 messagesService message.Service, // We need this to get the task tool messages
121 messagesService message.Service, // We need this to get the task tool messages
122 focusedUIMessageId string,
122 focusedUIMessageId string,
123+ isSummary bool,
123 width int,
124 width int,
124 position int,
125 position int,
125) []uiMessage {
126) []uiMessage {
@@ -168,6 +169,9 @@ func renderAssistantMessage(
168 if content == "" {
169 if content == "" {
169 content = "*Finished without output*"
170 content = "*Finished without output*"
170 }
171 }
172+ if isSummary {
173+ info = append(info, baseStyle.Width(width-1).Foreground(t.TextMuted()).Render(" (summary)"))
174+ }
171
175
172 content = renderMessage(content, false, true, width, info...)
176 content = renderMessage(content, false, true, width, info...)
173 messages = append(messages, uiMessage{
177 messages = append(messages, uiMessage{
internal/tui/tui.go−24
@@ -331,30 +331,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
331
331
332 if payload.Done && payload.Type == agent.AgentEventTypeSummarize {
332 if payload.Done && payload.Type == agent.AgentEventTypeSummarize {
333 a.isCompacting = false
333 a.isCompacting = false
334−
335− if payload.SessionID != "" {
336− // Switch to the new session
337− return a, func() tea.Msg {
338− sessions, err := a.app.Sessions.List(context.Background())
339− if err != nil {
340− return util.InfoMsg{
341− Type: util.InfoTypeError,
342− Msg: "Failed to list sessions: " + err.Error(),
343− }
344− }
345−
346− for _, s := range sessions {
347− if s.ID == payload.SessionID {
348− return dialog.SessionSelectedMsg{Session: s}
349− }
350− }
351−
352− return util.InfoMsg{
353− Type: util.InfoTypeError,
354− Msg: "Failed to find new session",
355− }
356− }
357− }
358 return a, util.ReportInfo("Session summarization complete")
334 return a, util.ReportInfo("Session summarization complete")
359 } else if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSession.ID != "" {
335 } else if payload.Done && payload.Type == agent.AgentEventTypeResponse && a.selectedSession.ID != "" {
360 model := a.app.CoderAgent.Model()
336 model := a.app.CoderAgent.Model()