mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: chatd refactor (#26270)
Implements the chatd stabilization RFC. Combines: - https://github.com/coder/coder/pull/25908 - https://github.com/coder/coder/pull/25923 - https://github.com/coder/coder/pull/26109 - https://github.com/coder/coder/pull/26110 - https://github.com/coder/coder/pull/26111 - https://github.com/coder/coder/pull/26112
This commit is contained in:
@@ -522,6 +522,60 @@ func (s *Service) TouchStep(
|
||||
})
|
||||
}
|
||||
|
||||
// TouchRun bumps the run's updated_at timestamp without changing any
|
||||
// other fields. Runner-owned debug turns use this while no model step is
|
||||
// active, such as requires-action waits.
|
||||
func (s *Service) TouchRun(ctx context.Context, runID uuid.UUID, chatID uuid.UUID) error {
|
||||
if s == nil || runID == uuid.Nil || chatID == uuid.Nil {
|
||||
return nil
|
||||
}
|
||||
return s.db.TouchChatDebugRunUpdatedAt(chatdContext(ctx),
|
||||
database.TouchChatDebugRunUpdatedAtParams{
|
||||
Now: s.clock.Now(),
|
||||
ID: runID,
|
||||
ChatID: chatID,
|
||||
})
|
||||
}
|
||||
|
||||
// LaunchRunHeartbeat starts a goroutine that periodically touches an
|
||||
// open run until done is closed or ctx is canceled.
|
||||
func (s *Service) LaunchRunHeartbeat(ctx context.Context, runID uuid.UUID, chatID uuid.UUID, done <-chan struct{}) {
|
||||
if s == nil || runID == uuid.Nil || chatID == uuid.Nil || done == nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
thresholdCh := s.thresholdChan()
|
||||
interval := s.heartbeatInterval()
|
||||
ticker := s.clock.NewTicker(interval, "chatdebug", "run-heartbeat")
|
||||
defer ticker.Stop()
|
||||
resetTicker := func() {
|
||||
if newInterval := s.heartbeatInterval(); newInterval != interval {
|
||||
interval = newInterval
|
||||
ticker.Reset(interval, "chatdebug", "run-heartbeat")
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-done:
|
||||
return
|
||||
case <-thresholdCh:
|
||||
thresholdCh = s.thresholdChan()
|
||||
resetTicker()
|
||||
case <-ticker.C:
|
||||
if err := s.TouchRun(ctx, runID, chatID); err != nil {
|
||||
s.log.Debug(ctx, "run heartbeat touch failed",
|
||||
slog.Error(err),
|
||||
slog.F("run_id", runID),
|
||||
)
|
||||
}
|
||||
resetTicker()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// DeleteByChatID deletes debug data for a chat and emits a delete event.
|
||||
// The startedBefore bound scopes deletion to runs created before that
|
||||
// instant so that retried cleanup does not remove runs created by a
|
||||
|
||||
@@ -41,28 +41,6 @@ func SeedSummary(label string) map[string]any {
|
||||
return map[string]any{"first_message": label}
|
||||
}
|
||||
|
||||
// ExtractFirstUserText extracts the plain text content from a
|
||||
// fantasy.Prompt for the first user message. Used to derive
|
||||
// first_message labels at run creation time.
|
||||
func ExtractFirstUserText(prompt fantasy.Prompt) string {
|
||||
for _, msg := range prompt {
|
||||
if msg.Role != fantasy.MessageRoleUser {
|
||||
continue
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, part := range msg.Content {
|
||||
tp, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, _ = sb.WriteString(tp.Text)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AggregateRunSummary reads all steps for the given run, computes token
|
||||
// totals, and merges them with the run's existing summary (preserving any
|
||||
// seeded first_message label). The baseSummary parameter should be the
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"charm.land/fantasy"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -62,68 +61,6 @@ func TestSeedSummary(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractFirstUserText(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("EmptyPrompt", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := chatdebug.ExtractFirstUserText(fantasy.Prompt{})
|
||||
require.Equal(t, "", got)
|
||||
})
|
||||
|
||||
t.Run("NoUserMessages", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prompt := fantasy.Prompt{
|
||||
{
|
||||
Role: fantasy.MessageRoleSystem,
|
||||
Content: []fantasy.MessagePart{fantasy.TextPart{Text: "system"}},
|
||||
},
|
||||
{
|
||||
Role: fantasy.MessageRoleAssistant,
|
||||
Content: []fantasy.MessagePart{fantasy.TextPart{Text: "assistant"}},
|
||||
},
|
||||
}
|
||||
got := chatdebug.ExtractFirstUserText(prompt)
|
||||
require.Equal(t, "", got)
|
||||
})
|
||||
|
||||
t.Run("FirstUserMessageMixedParts", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prompt := fantasy.Prompt{
|
||||
{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
Content: []fantasy.MessagePart{
|
||||
fantasy.TextPart{Text: "hello "},
|
||||
fantasy.FilePart{Filename: "test.png"},
|
||||
fantasy.TextPart{Text: "world"},
|
||||
},
|
||||
},
|
||||
}
|
||||
got := chatdebug.ExtractFirstUserText(prompt)
|
||||
require.Equal(t, "hello world", got)
|
||||
})
|
||||
|
||||
t.Run("MultipleUserMessagesReturnsFirst", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
prompt := fantasy.Prompt{
|
||||
{
|
||||
Role: fantasy.MessageRoleSystem,
|
||||
Content: []fantasy.MessagePart{fantasy.TextPart{Text: "system"}},
|
||||
},
|
||||
{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
Content: []fantasy.MessagePart{fantasy.TextPart{Text: "first"}},
|
||||
},
|
||||
{
|
||||
Role: fantasy.MessageRoleUser,
|
||||
Content: []fantasy.MessagePart{fantasy.TextPart{Text: "second"}},
|
||||
},
|
||||
}
|
||||
got := chatdebug.ExtractFirstUserText(prompt)
|
||||
require.Equal(t, "first", got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_AggregateRunSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user