Hugo Dutka
2026-06-12 13:33:12 +02:00
committed by GitHub
parent 4a07f61c50
commit 4debd23cbb
155 changed files with 37612 additions and 24513 deletions
+54
View File
@@ -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
-22
View File
@@ -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
-63
View File
@@ -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()