- Added `OutboundStreamMessage` type to represent streaming chunks. - Added `outboundStream` channel to `MessageBus` with non-blocking send logic (`ErrBusFull`) to prevent stalling the LLM loop when consumers are unavailable or slow. - Updated `processOptions` to include a `Stream` boolean. - Updated `executeLLMWithRetry` to inject a `stream_callback` into `llmOpts` that pushes token chunks to `PublishOutboundStream`. - Updated `AGENT_LOOP_IMPROVEMENTS.md` marking the task as complete. Co-authored-by: hobbyistlabs-coder <267281733+hobbyistlabs-coder@users.noreply.github.com>
202 lines
4.2 KiB
Go
202 lines
4.2 KiB
Go
package bus
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync/atomic"
|
|
|
|
"jane/pkg/logger"
|
|
)
|
|
|
|
// ErrBusClosed is returned when publishing to a closed MessageBus.
|
|
var ErrBusClosed = errors.New("message bus closed")
|
|
|
|
// ErrBusFull is returned when publishing to a full MessageBus.
|
|
var ErrBusFull = errors.New("message bus full")
|
|
|
|
const defaultBusBufferSize = 64
|
|
|
|
type MessageBus struct {
|
|
inbound chan InboundMessage
|
|
outbound chan OutboundMessage
|
|
outboundStream chan OutboundStreamMessage
|
|
outboundMedia chan OutboundMediaMessage
|
|
done chan struct{}
|
|
closed atomic.Bool
|
|
}
|
|
|
|
func NewMessageBus() *MessageBus {
|
|
return &MessageBus{
|
|
inbound: make(chan InboundMessage, defaultBusBufferSize),
|
|
outbound: make(chan OutboundMessage, defaultBusBufferSize),
|
|
outboundStream: make(chan OutboundStreamMessage, defaultBusBufferSize),
|
|
outboundMedia: make(chan OutboundMediaMessage, defaultBusBufferSize),
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) PublishInbound(ctx context.Context, msg InboundMessage) error {
|
|
if mb.closed.Load() {
|
|
return ErrBusClosed
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case mb.inbound <- msg:
|
|
return nil
|
|
case <-mb.done:
|
|
return ErrBusClosed
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) ConsumeInbound(ctx context.Context) (InboundMessage, bool) {
|
|
select {
|
|
case msg, ok := <-mb.inbound:
|
|
return msg, ok
|
|
case <-mb.done:
|
|
return InboundMessage{}, false
|
|
case <-ctx.Done():
|
|
return InboundMessage{}, false
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) PublishOutbound(ctx context.Context, msg OutboundMessage) error {
|
|
if mb.closed.Load() {
|
|
return ErrBusClosed
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case mb.outbound <- msg:
|
|
return nil
|
|
case <-mb.done:
|
|
return ErrBusClosed
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) SubscribeOutbound(ctx context.Context) (OutboundMessage, bool) {
|
|
select {
|
|
case msg, ok := <-mb.outbound:
|
|
return msg, ok
|
|
case <-mb.done:
|
|
return OutboundMessage{}, false
|
|
case <-ctx.Done():
|
|
return OutboundMessage{}, false
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) PublishOutboundMedia(ctx context.Context, msg OutboundMediaMessage) error {
|
|
if mb.closed.Load() {
|
|
return ErrBusClosed
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case mb.outboundMedia <- msg:
|
|
return nil
|
|
case <-mb.done:
|
|
return ErrBusClosed
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) SubscribeOutboundMedia(ctx context.Context) (OutboundMediaMessage, bool) {
|
|
select {
|
|
case msg, ok := <-mb.outboundMedia:
|
|
return msg, ok
|
|
case <-mb.done:
|
|
return OutboundMediaMessage{}, false
|
|
case <-ctx.Done():
|
|
return OutboundMediaMessage{}, false
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) PublishOutboundStream(ctx context.Context, msg OutboundStreamMessage) error {
|
|
if mb.closed.Load() {
|
|
return ErrBusClosed
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case mb.outboundStream <- msg:
|
|
return nil
|
|
case <-mb.done:
|
|
return ErrBusClosed
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
// Non-blocking send. If the buffer is full, immediately drop the token.
|
|
return ErrBusFull
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) SubscribeOutboundStream(ctx context.Context) (OutboundStreamMessage, bool) {
|
|
select {
|
|
case msg, ok := <-mb.outboundStream:
|
|
return msg, ok
|
|
case <-mb.done:
|
|
return OutboundStreamMessage{}, false
|
|
case <-ctx.Done():
|
|
return OutboundStreamMessage{}, false
|
|
}
|
|
}
|
|
|
|
func (mb *MessageBus) Close() {
|
|
if mb.closed.CompareAndSwap(false, true) {
|
|
close(mb.done)
|
|
|
|
// Drain buffered channels so messages aren't silently lost.
|
|
// Channels are NOT closed to avoid send-on-closed panics from concurrent publishers.
|
|
drained := 0
|
|
for {
|
|
select {
|
|
case <-mb.inbound:
|
|
drained++
|
|
default:
|
|
goto doneInbound
|
|
}
|
|
}
|
|
doneInbound:
|
|
for {
|
|
select {
|
|
case <-mb.outbound:
|
|
drained++
|
|
default:
|
|
goto doneOutbound
|
|
}
|
|
}
|
|
doneOutbound:
|
|
for {
|
|
select {
|
|
case <-mb.outboundStream:
|
|
drained++
|
|
default:
|
|
goto doneStream
|
|
}
|
|
}
|
|
doneStream:
|
|
for {
|
|
select {
|
|
case <-mb.outboundMedia:
|
|
drained++
|
|
default:
|
|
goto doneMedia
|
|
}
|
|
}
|
|
doneMedia:
|
|
if drained > 0 {
|
|
logger.DebugCF("bus", "Drained buffered messages during close", map[string]any{
|
|
"count": drained,
|
|
})
|
|
}
|
|
}
|
|
}
|