Fix joining active inference

This commit is contained in:
dwrz
2026-11-04 00:25:23 +00:00
parent e13d9d849d
commit caaacf1c98
4 changed files with 138 additions and 124 deletions
+25 -5
View File
@@ -155,6 +155,7 @@ type EventType string
const (
EventAssistantUpdate EventType = "assistant_update"
EventDone EventType = "done"
EventInferenceActive EventType = "inference_active"
EventMessageDeleted EventType = "message_deleted"
EventMessageSaved EventType = "message_saved"
)
@@ -197,6 +198,22 @@ type Done struct {
func (Done) Type() EventType { return EventDone }
// InferenceActive reports that an [Inference] is reserved or in progress.
// [Conversation.Subscribe] sends this event one time, after the cache diff
// and before the live events of the run.
//
// Without this event, two cases look the same to the client:
// - No run is active. The stream closes after the diff.
// - A run is active, but it sends no events yet. For example, the model
// has not sent its first token, or a slow tool is running.
//
// A client that did not start the run uses this event to show the stop
// control. The client also uses this event to separate saved history
// (before the event) from live events (after the event).
type InferenceActive struct{}
func (InferenceActive) Type() EventType { return EventInferenceActive }
// Inference is one reserved inference call and its events.
// [Conversation.Reserve] creates it, and [Inference.Run] runs it.
// [Inference.Events] and [Conversation.Subscribe] use its events to send
@@ -323,11 +340,11 @@ func (inf *Inference) follow(
// 1. Cache diff: [MessageSaved] for each saved message missing from or
// newer than manifest, followed by [MessageDeleted] for each ID in
// manifest that is no longer saved.
// 2. Active inference: if an [Inference] is reserved or running when
// Subscribe loads the database, Subscribe streams its
// [MessageSaved] and [AssistantUpdate] events, then ends with [Done].
// If no inference is reserved or running, Subscribe sends no [Done]
// event.
// 2. Active inference: if an [Inference] is reserved or in progress when
// this method reads the database, the stream sends [InferenceActive],
// then the [MessageSaved] and [AssistantUpdate] events of the run, and
// then [Done]. If no run is active, the stream closes after the diff and
// does not send [InferenceActive] or [Done].
//
// The channel closes when delivery finishes or ctx is canceled. Canceling
// ctx stops delivery to this subscriber; it does not stop an ongoing
@@ -372,6 +389,9 @@ func (c *Conversation) Subscribe(
if inf == nil {
return
}
if !send(ctx, ch, InferenceActive{}) {
return
}
// Skip events for messages that the initial diff already
// sent. If the client's manifest already had a message from
// this call, it is not in sent and will be re-sent here; the
+17 -3
View File
@@ -1474,17 +1474,21 @@ func TestSubscribeInference(t *testing.T) {
close(release)
got := collectEvents(t, ch)
// The diff delivers the user message first.
// The cache diff sends the user message first. Then, the active marker
// shows that the live events of the run start.
saved, ok := got[0].(MessageSaved)
if !ok || saved.Message.Role != db.MessageRoleUser {
t.Fatalf("first event = %v, want the user MessageSaved", got[0])
}
if _, ok := got[1].(InferenceActive); !ok {
t.Fatalf("second event = %v, want InferenceActive", got[1])
}
var (
chunkTexts []string
chunkIDs []string
asst *MessageSaved
)
for _, e := range got[1:] {
for _, e := range got[2:] {
switch e := e.(type) {
case AssistantUpdate:
if asst != nil {
@@ -1589,6 +1593,10 @@ func TestSubscribeInferenceError(t *testing.T) {
close(release)
got := collectEvents(t, ch)
// The active marker comes after the user message from the cache diff.
if _, ok := got[1].(InferenceActive); !ok {
t.Fatalf("second event = %v, want InferenceActive", got[1])
}
done, ok := got[len(got)-1].(Done)
if !ok {
t.Fatalf("last event = %v, want Done", got[len(got)-1])
@@ -1709,12 +1717,18 @@ func TestSubscribeToolResult(t *testing.T) {
t.Errorf("event %d role = %q, want the user first", i, e.Message.Role)
}
}
case InferenceActive:
// This event separates the cache diff from the live events
// of the run.
case Done:
if e.Error != "" {
t.Errorf("Done.Error = %q, want empty", e.Error)
}
default:
t.Errorf("event %d = %v, want MessageSaved or Done", i, e)
t.Errorf(
"event %d = %v, want MessageSaved, InferenceActive, or Done",
i, e,
)
}
}
if aId == "" {