refactor(events): remove legacy agent event bus
Drop the old agent EventBus, SubscribeEvents/EventDrops public surface, legacy hook observer dispatch, and hook.event process notification path. Agent observations now flow through pkg/events runtime events. Validation: go test ./pkg/agent; make lint
This commit is contained in:
parent
fce800414d
commit
b954e6b8dc
8 changed files with 8 additions and 365 deletions
|
|
@ -38,8 +38,7 @@ type AgentLoop struct {
|
||||||
registry *AgentRegistry
|
registry *AgentRegistry
|
||||||
state *state.Manager
|
state *state.Manager
|
||||||
|
|
||||||
// Event system (from Incoming)
|
// Runtime event system
|
||||||
eventBus *EventBus
|
|
||||||
runtimeEvents runtimeevents.Bus
|
runtimeEvents runtimeevents.Bus
|
||||||
ownsRuntimeEvents bool
|
ownsRuntimeEvents bool
|
||||||
hooks *HookManager
|
hooks *HookManager
|
||||||
|
|
@ -286,9 +285,6 @@ func (al *AgentLoop) Close() {
|
||||||
if al.hooks != nil {
|
if al.hooks != nil {
|
||||||
al.hooks.Close()
|
al.hooks.Close()
|
||||||
}
|
}
|
||||||
if al.eventBus != nil {
|
|
||||||
al.eventBus.Close()
|
|
||||||
}
|
|
||||||
if al.runtimeEvents != nil && al.ownsRuntimeEvents {
|
if al.runtimeEvents != nil && al.ownsRuntimeEvents {
|
||||||
if err := al.runtimeEvents.Close(); err != nil {
|
if err := al.runtimeEvents.Close(); err != nil {
|
||||||
logger.ErrorCF("agent", "Failed to close runtime event bus",
|
logger.ErrorCF("agent", "Failed to close runtime event bus",
|
||||||
|
|
@ -303,12 +299,6 @@ func (al *AgentLoop) Close() {
|
||||||
|
|
||||||
// UnmountHook removes a previously registered in-process hook.
|
// UnmountHook removes a previously registered in-process hook.
|
||||||
|
|
||||||
// SubscribeEvents registers a subscriber for agent-loop events.
|
|
||||||
|
|
||||||
// UnsubscribeEvents removes a previously registered event subscriber.
|
|
||||||
|
|
||||||
// EventDrops returns the number of dropped events for the given kind.
|
|
||||||
|
|
||||||
type turnEventScope struct {
|
type turnEventScope struct {
|
||||||
agentID string
|
agentID string
|
||||||
sessionKey string
|
sessionKey string
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,6 @@ func (al *AgentLoop) emitEvent(kind EventKind, meta EventMeta, payload any) {
|
||||||
|
|
||||||
al.logEvent(evt)
|
al.logEvent(evt)
|
||||||
|
|
||||||
if al.eventBus != nil {
|
|
||||||
al.eventBus.Emit(evt)
|
|
||||||
}
|
|
||||||
al.publishRuntimeEvent(evt)
|
al.publishRuntimeEvent(evt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,38 +162,6 @@ func (al *AgentLoop) UnmountHook(name string) {
|
||||||
al.hooks.Unmount(name)
|
al.hooks.Unmount(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscribeEvents registers a subscriber for agent-loop events.
|
|
||||||
//
|
|
||||||
// Deprecated: use RuntimeEvents for new event observation code.
|
|
||||||
func (al *AgentLoop) SubscribeEvents(buffer int) EventSubscription {
|
|
||||||
if al == nil || al.eventBus == nil {
|
|
||||||
ch := make(chan Event)
|
|
||||||
close(ch)
|
|
||||||
return EventSubscription{C: ch}
|
|
||||||
}
|
|
||||||
return al.eventBus.Subscribe(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnsubscribeEvents removes a previously registered event subscriber.
|
|
||||||
//
|
|
||||||
// Deprecated: use the Subscription returned by RuntimeEvents instead.
|
|
||||||
func (al *AgentLoop) UnsubscribeEvents(id uint64) {
|
|
||||||
if al == nil || al.eventBus == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
al.eventBus.Unsubscribe(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventDrops returns the number of dropped events for the given kind.
|
|
||||||
//
|
|
||||||
// Deprecated: use RuntimeEventStats for runtime event drop counters.
|
|
||||||
func (al *AgentLoop) EventDrops(kind EventKind) int64 {
|
|
||||||
if al == nil || al.eventBus == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return al.eventBus.Dropped(kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RuntimeEvents returns the root runtime event channel.
|
// RuntimeEvents returns the root runtime event channel.
|
||||||
func (al *AgentLoop) RuntimeEvents() runtimeevents.EventChannel {
|
func (al *AgentLoop) RuntimeEvents() runtimeevents.EventChannel {
|
||||||
if al == nil || al.runtimeEvents == nil {
|
if al == nil || al.runtimeEvents == nil {
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,6 @@ func NewAgentLoop(
|
||||||
stateManager = state.NewManager(defaultAgent.Workspace)
|
stateManager = state.NewManager(defaultAgent.Workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
eventBus := NewEventBus()
|
|
||||||
|
|
||||||
// Determine worker pool size from config (default: 1 = sequential)
|
// Determine worker pool size from config (default: 1 = sequential)
|
||||||
workerPoolSize := cfg.Agents.Defaults.MaxParallelTurns
|
workerPoolSize := cfg.Agents.Defaults.MaxParallelTurns
|
||||||
if workerPoolSize <= 0 {
|
if workerPoolSize <= 0 {
|
||||||
|
|
@ -62,7 +60,6 @@ func NewAgentLoop(
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
registry: registry,
|
registry: registry,
|
||||||
state: stateManager,
|
state: stateManager,
|
||||||
eventBus: eventBus,
|
|
||||||
fallback: fallbackChain,
|
fallback: fallbackChain,
|
||||||
cmdRegistry: commands.NewRegistry(commands.BuiltinDefinitions()),
|
cmdRegistry: commands.NewRegistry(commands.BuiltinDefinitions()),
|
||||||
steering: newSteeringQueue(parseSteeringMode(cfg.Agents.Defaults.SteeringMode)),
|
steering: newSteeringQueue(parseSteeringMode(cfg.Agents.Defaults.SteeringMode)),
|
||||||
|
|
@ -79,7 +76,7 @@ func NewAgentLoop(
|
||||||
al.ownsRuntimeEvents = true
|
al.ownsRuntimeEvents = true
|
||||||
}
|
}
|
||||||
al.providerFactory = providers.CreateProviderFromConfig
|
al.providerFactory = providers.CreateProviderFromConfig
|
||||||
al.hooks = NewHookManagerWithRuntimeEvents(eventBus, al.runtimeEvents.Channel())
|
al.hooks = NewHookManager(al.runtimeEvents.Channel())
|
||||||
configureHookManagerFromConfig(al.hooks, cfg)
|
configureHookManagerFromConfig(al.hooks, cfg)
|
||||||
al.contextManager = al.resolveContextManager()
|
al.contextManager = al.resolveContextManager()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
package agent
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultEventSubscriberBuffer = 16
|
|
||||||
|
|
||||||
// EventSubscription identifies a subscriber channel returned by EventBus.Subscribe.
|
|
||||||
//
|
|
||||||
// Deprecated: use pkg/events.Subscription from RuntimeEvents for new code.
|
|
||||||
type EventSubscription struct {
|
|
||||||
ID uint64
|
|
||||||
C <-chan Event
|
|
||||||
}
|
|
||||||
|
|
||||||
type eventSubscriber struct {
|
|
||||||
ch chan Event
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventBus is a lightweight multi-subscriber broadcaster for agent-loop events.
|
|
||||||
//
|
|
||||||
// Deprecated: use pkg/events.EventBus for new code. This legacy bus remains
|
|
||||||
// only while existing agent event consumers migrate.
|
|
||||||
type EventBus struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
subs map[uint64]eventSubscriber
|
|
||||||
nextID uint64
|
|
||||||
closed bool
|
|
||||||
dropped [eventKindCount]atomic.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEventBus creates a new in-process event broadcaster.
|
|
||||||
//
|
|
||||||
// Deprecated: use events.NewBus for new runtime event publishers.
|
|
||||||
func NewEventBus() *EventBus {
|
|
||||||
return &EventBus{
|
|
||||||
subs: make(map[uint64]eventSubscriber),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe registers a new subscriber with the requested channel buffer size.
|
|
||||||
// A non-positive buffer uses the default size.
|
|
||||||
func (b *EventBus) Subscribe(buffer int) EventSubscription {
|
|
||||||
if buffer <= 0 {
|
|
||||||
buffer = defaultEventSubscriberBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
|
|
||||||
if b.closed {
|
|
||||||
ch := make(chan Event)
|
|
||||||
close(ch)
|
|
||||||
return EventSubscription{C: ch}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.nextID++
|
|
||||||
id := b.nextID
|
|
||||||
ch := make(chan Event, buffer)
|
|
||||||
b.subs[id] = eventSubscriber{ch: ch}
|
|
||||||
return EventSubscription{ID: id, C: ch}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unsubscribe removes a subscriber and closes its channel.
|
|
||||||
func (b *EventBus) Unsubscribe(id uint64) {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
|
|
||||||
sub, ok := b.subs[id]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(b.subs, id)
|
|
||||||
close(sub.ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit broadcasts an event to all current subscribers without blocking.
|
|
||||||
// When a subscriber channel is full, the event is dropped for that subscriber.
|
|
||||||
func (b *EventBus) Emit(evt Event) {
|
|
||||||
if evt.Time.IsZero() {
|
|
||||||
evt.Time = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.mu.RLock()
|
|
||||||
defer b.mu.RUnlock()
|
|
||||||
|
|
||||||
if b.closed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sub := range b.subs {
|
|
||||||
select {
|
|
||||||
case sub.ch <- evt:
|
|
||||||
default:
|
|
||||||
if evt.Kind < eventKindCount {
|
|
||||||
b.dropped[evt.Kind].Add(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dropped returns the number of dropped events for a given kind.
|
|
||||||
func (b *EventBus) Dropped(kind EventKind) int64 {
|
|
||||||
if kind >= eventKindCount {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return b.dropped[kind].Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes all subscriber channels and stops future broadcasts.
|
|
||||||
func (b *EventBus) Close() {
|
|
||||||
b.mu.Lock()
|
|
||||||
defer b.mu.Unlock()
|
|
||||||
|
|
||||||
if b.closed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
b.closed = true
|
|
||||||
for id, sub := range b.subs {
|
|
||||||
close(sub.ch)
|
|
||||||
delete(b.subs, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,74 +16,17 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/tools"
|
"github.com/sipeed/picoclaw/pkg/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEventBus_SubscribeEmitUnsubscribeClose(t *testing.T) {
|
func TestAgentLoop_PublishesRuntimeEvents(t *testing.T) {
|
||||||
eventBus := NewEventBus()
|
|
||||||
sub := eventBus.Subscribe(1)
|
|
||||||
|
|
||||||
eventBus.Emit(Event{
|
|
||||||
Kind: EventKindTurnStart,
|
|
||||||
Meta: EventMeta{TurnID: "turn-1"},
|
|
||||||
})
|
|
||||||
|
|
||||||
select {
|
|
||||||
case evt := <-sub.C:
|
|
||||||
if evt.Kind != EventKindTurnStart {
|
|
||||||
t.Fatalf("expected %v, got %v", EventKindTurnStart, evt.Kind)
|
|
||||||
}
|
|
||||||
if evt.Meta.TurnID != "turn-1" {
|
|
||||||
t.Fatalf("expected turn id turn-1, got %q", evt.Meta.TurnID)
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatal("timed out waiting for event")
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.Unsubscribe(sub.ID)
|
|
||||||
if _, ok := <-sub.C; ok {
|
|
||||||
t.Fatal("expected subscriber channel to be closed after unsubscribe")
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.Close()
|
|
||||||
closedSub := eventBus.Subscribe(1)
|
|
||||||
if _, ok := <-closedSub.C; ok {
|
|
||||||
t.Fatal("expected closed bus to return a closed subscriber channel")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEventBus_DropsWhenSubscriberIsFull(t *testing.T) {
|
|
||||||
eventBus := NewEventBus()
|
|
||||||
sub := eventBus.Subscribe(1)
|
|
||||||
defer eventBus.Unsubscribe(sub.ID)
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
eventBus.Emit(Event{Kind: EventKindLLMRequest})
|
|
||||||
}
|
|
||||||
|
|
||||||
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
|
||||||
t.Fatalf("Emit took too long with a blocked subscriber: %s", elapsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := eventBus.Dropped(EventKindLLMRequest); got != 999 {
|
|
||||||
t.Fatalf("expected 999 dropped events, got %d", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAgentLoop_DualPublishesRuntimeEvents(t *testing.T) {
|
|
||||||
runtimeBus := runtimeevents.NewBus()
|
runtimeBus := runtimeevents.NewBus()
|
||||||
al := &AgentLoop{
|
al := &AgentLoop{
|
||||||
eventBus: NewEventBus(),
|
|
||||||
runtimeEvents: runtimeBus,
|
runtimeEvents: runtimeBus,
|
||||||
}
|
}
|
||||||
defer al.eventBus.Close()
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := runtimeBus.Close(); err != nil {
|
if err := runtimeBus.Close(); err != nil {
|
||||||
t.Errorf("runtime bus close failed: %v", err)
|
t.Errorf("runtime bus close failed: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
legacySub := al.SubscribeEvents(1)
|
|
||||||
defer al.UnsubscribeEvents(legacySub.ID)
|
|
||||||
|
|
||||||
runtimeSub, runtimeCh, err := al.RuntimeEvents().OfKind(runtimeevents.KindAgentToolExecStart).SubscribeChan(
|
runtimeSub, runtimeCh, err := al.RuntimeEvents().OfKind(runtimeevents.KindAgentToolExecStart).SubscribeChan(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
runtimeevents.SubscribeOptions{Name: "runtime", Buffer: 1},
|
runtimeevents.SubscribeOptions{Name: "runtime", Buffer: 1},
|
||||||
|
|
@ -122,11 +65,6 @@ func TestAgentLoop_DualPublishesRuntimeEvents(t *testing.T) {
|
||||||
ToolExecStartPayload{Tool: "mock_custom", Arguments: map[string]any{"task": "ping"}},
|
ToolExecStartPayload{Tool: "mock_custom", Arguments: map[string]any{"task": "ping"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
legacyEvt := receiveLegacyEvent(t, legacySub.C)
|
|
||||||
if legacyEvt.Kind != EventKindToolExecStart {
|
|
||||||
t.Fatalf("legacy kind = %v, want %v", legacyEvt.Kind, EventKindToolExecStart)
|
|
||||||
}
|
|
||||||
|
|
||||||
runtimeEvt := receiveRuntimeEvent(t, runtimeCh)
|
runtimeEvt := receiveRuntimeEvent(t, runtimeCh)
|
||||||
if runtimeEvt.Kind != runtimeevents.KindAgentToolExecStart {
|
if runtimeEvt.Kind != runtimeevents.KindAgentToolExecStart {
|
||||||
t.Fatalf("runtime kind = %q, want %q", runtimeEvt.Kind, runtimeevents.KindAgentToolExecStart)
|
t.Fatalf("runtime kind = %q, want %q", runtimeEvt.Kind, runtimeevents.KindAgentToolExecStart)
|
||||||
|
|
@ -736,21 +674,6 @@ func TestAgentLoop_EmitsFollowUpQueuedEvent(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func receiveLegacyEvent(t *testing.T, ch <-chan Event) Event {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case evt, ok := <-ch:
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("legacy event stream closed before expected event arrived")
|
|
||||||
}
|
|
||||||
return evt
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatal("timed out waiting for legacy event")
|
|
||||||
return Event{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func receiveRuntimeEvent(t *testing.T, ch <-chan runtimeevents.Event) runtimeevents.Event {
|
func receiveRuntimeEvent(t *testing.T, ch <-chan runtimeevents.Event) runtimeevents.Event {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,19 +184,6 @@ func (ph *ProcessHook) Close() error {
|
||||||
return ph.closeErr
|
return ph.closeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ph *ProcessHook) OnEvent(ctx context.Context, evt Event) error {
|
|
||||||
if ph == nil || !ph.opts.Observe {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(ph.observeKinds) > 0 {
|
|
||||||
kind := runtimeKindForAgentEvent(evt.Kind).String()
|
|
||||||
if _, ok := ph.observeKinds[kind]; !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ph.notify(ctx, "hook.event", evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ph *ProcessHook) OnRuntimeEvent(ctx context.Context, evt runtimeevents.Event) error {
|
func (ph *ProcessHook) OnRuntimeEvent(ctx context.Context, evt runtimeevents.Event) error {
|
||||||
if ph == nil || !ph.opts.Observe {
|
if ph == nil || !ph.opts.Observe {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,6 @@ func NamedHook(name string, hook any) HookRegistration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type EventObserver interface {
|
|
||||||
// Deprecated: implement RuntimeEventObserver for new observation code.
|
|
||||||
OnEvent(ctx context.Context, evt Event) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuntimeEventObserver interface {
|
type RuntimeEventObserver interface {
|
||||||
OnRuntimeEvent(ctx context.Context, evt runtimeevents.Event) error
|
OnRuntimeEvent(ctx context.Context, evt runtimeevents.Event) error
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +193,6 @@ func (r *ToolResultHookResponse) Clone() *ToolResultHookResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
type HookManager struct {
|
type HookManager struct {
|
||||||
eventBus *EventBus
|
|
||||||
runtimeEvents runtimeevents.EventChannel
|
runtimeEvents runtimeevents.EventChannel
|
||||||
observerTimeout time.Duration
|
observerTimeout time.Duration
|
||||||
interceptorTimeout time.Duration
|
interceptorTimeout time.Duration
|
||||||
|
|
@ -208,37 +202,21 @@ type HookManager struct {
|
||||||
hooks map[string]HookRegistration
|
hooks map[string]HookRegistration
|
||||||
ordered []HookRegistration
|
ordered []HookRegistration
|
||||||
|
|
||||||
sub EventSubscription
|
runtimeSub runtimeevents.Subscription
|
||||||
runtimeSub runtimeevents.Subscription
|
runtimeDone chan struct{}
|
||||||
done chan struct{}
|
closeOnce sync.Once
|
||||||
runtimeDone chan struct{}
|
|
||||||
runtimeObserveEnabled bool
|
|
||||||
closeOnce sync.Once
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHookManager(eventBus *EventBus) *HookManager {
|
func NewHookManager(runtimeEvents runtimeevents.EventChannel) *HookManager {
|
||||||
return NewHookManagerWithRuntimeEvents(eventBus, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHookManagerWithRuntimeEvents(eventBus *EventBus, runtimeEvents runtimeevents.EventChannel) *HookManager {
|
|
||||||
hm := &HookManager{
|
hm := &HookManager{
|
||||||
eventBus: eventBus,
|
|
||||||
runtimeEvents: runtimeEvents,
|
runtimeEvents: runtimeEvents,
|
||||||
observerTimeout: defaultHookObserverTimeout,
|
observerTimeout: defaultHookObserverTimeout,
|
||||||
interceptorTimeout: defaultHookInterceptorTimeout,
|
interceptorTimeout: defaultHookInterceptorTimeout,
|
||||||
approvalTimeout: defaultHookApprovalTimeout,
|
approvalTimeout: defaultHookApprovalTimeout,
|
||||||
hooks: make(map[string]HookRegistration),
|
hooks: make(map[string]HookRegistration),
|
||||||
done: make(chan struct{}),
|
|
||||||
runtimeDone: make(chan struct{}),
|
runtimeDone: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if eventBus != nil {
|
|
||||||
hm.sub = eventBus.Subscribe(hookObserverBufferSize)
|
|
||||||
go hm.dispatchEvents()
|
|
||||||
} else {
|
|
||||||
close(hm.done)
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtimeEvents != nil {
|
if runtimeEvents != nil {
|
||||||
sub, ch, err := runtimeEvents.SubscribeChan(context.Background(), runtimeevents.SubscribeOptions{
|
sub, ch, err := runtimeEvents.SubscribeChan(context.Background(), runtimeevents.SubscribeOptions{
|
||||||
Name: "hook-manager-observer",
|
Name: "hook-manager-observer",
|
||||||
|
|
@ -251,7 +229,6 @@ func NewHookManagerWithRuntimeEvents(eventBus *EventBus, runtimeEvents runtimeev
|
||||||
close(hm.runtimeDone)
|
close(hm.runtimeDone)
|
||||||
} else {
|
} else {
|
||||||
hm.runtimeSub = sub
|
hm.runtimeSub = sub
|
||||||
hm.runtimeObserveEnabled = true
|
|
||||||
go hm.dispatchRuntimeEvents(ch)
|
go hm.dispatchRuntimeEvents(ch)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -267,9 +244,6 @@ func (hm *HookManager) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
hm.closeOnce.Do(func() {
|
hm.closeOnce.Do(func() {
|
||||||
if hm.eventBus != nil {
|
|
||||||
hm.eventBus.Unsubscribe(hm.sub.ID)
|
|
||||||
}
|
|
||||||
if hm.runtimeSub != nil {
|
if hm.runtimeSub != nil {
|
||||||
if err := hm.runtimeSub.Close(); err != nil {
|
if err := hm.runtimeSub.Close(); err != nil {
|
||||||
logger.WarnCF("hooks", "Failed to close runtime event hook subscription", map[string]any{
|
logger.WarnCF("hooks", "Failed to close runtime event hook subscription", map[string]any{
|
||||||
|
|
@ -277,7 +251,6 @@ func (hm *HookManager) Close() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<-hm.done
|
|
||||||
<-hm.runtimeDone
|
<-hm.runtimeDone
|
||||||
hm.closeAllHooks()
|
hm.closeAllHooks()
|
||||||
})
|
})
|
||||||
|
|
@ -335,25 +308,6 @@ func (hm *HookManager) Unmount(name string) {
|
||||||
hm.rebuildOrdered()
|
hm.rebuildOrdered()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hm *HookManager) dispatchEvents() {
|
|
||||||
defer close(hm.done)
|
|
||||||
|
|
||||||
for evt := range hm.sub.C {
|
|
||||||
for _, reg := range hm.snapshotHooks() {
|
|
||||||
if hm.runtimeObserveEnabled {
|
|
||||||
if _, ok := reg.Hook.(RuntimeEventObserver); ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
observer, ok := reg.Hook.(EventObserver)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hm.runObserver(reg.Name, observer, evt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hm *HookManager) dispatchRuntimeEvents(ch <-chan runtimeevents.Event) {
|
func (hm *HookManager) dispatchRuntimeEvents(ch <-chan runtimeevents.Event) {
|
||||||
defer close(hm.runtimeDone)
|
defer close(hm.runtimeDone)
|
||||||
|
|
||||||
|
|
@ -643,33 +597,6 @@ func (hm *HookManager) closeAllHooks() {
|
||||||
hm.ordered = nil
|
hm.ordered = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hm *HookManager) runObserver(name string, observer EventObserver, evt Event) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), hm.observerTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
done <- observer.OnEvent(ctx, evt)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
if err != nil {
|
|
||||||
logger.WarnCF("hooks", "Event observer failed", map[string]any{
|
|
||||||
"hook": name,
|
|
||||||
"event": evt.Kind.String(),
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
logger.WarnCF("hooks", "Event observer timed out", map[string]any{
|
|
||||||
"hook": name,
|
|
||||||
"event": evt.Kind.String(),
|
|
||||||
"timeout_ms": hm.observerTimeout.Milliseconds(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hm *HookManager) runRuntimeObserver(
|
func (hm *HookManager) runRuntimeObserver(
|
||||||
name string,
|
name string,
|
||||||
observer RuntimeEventObserver,
|
observer RuntimeEventObserver,
|
||||||
|
|
|
||||||
|
|
@ -152,20 +152,9 @@ func (h *llmObserverHook) AfterLLM(
|
||||||
}
|
}
|
||||||
|
|
||||||
type dualRuntimeObserverHook struct {
|
type dualRuntimeObserverHook struct {
|
||||||
legacyCh chan Event
|
|
||||||
runtimeCh chan runtimeevents.Event
|
runtimeCh chan runtimeevents.Event
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *dualRuntimeObserverHook) OnEvent(ctx context.Context, evt Event) error {
|
|
||||||
if evt.Kind == EventKindTurnEnd {
|
|
||||||
select {
|
|
||||||
case h.legacyCh <- evt:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *dualRuntimeObserverHook) OnRuntimeEvent(ctx context.Context, evt runtimeevents.Event) error {
|
func (h *dualRuntimeObserverHook) OnRuntimeEvent(ctx context.Context, evt runtimeevents.Event) error {
|
||||||
if evt.Kind == runtimeevents.KindAgentTurnEnd {
|
if evt.Kind == runtimeevents.KindAgentTurnEnd {
|
||||||
select {
|
select {
|
||||||
|
|
@ -522,13 +511,12 @@ func TestAgentLoop_Hooks_ObserverAndLLMInterceptor(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAgentLoop_Hooks_RuntimeObserverPreferredOverLegacyObserver(t *testing.T) {
|
func TestAgentLoop_Hooks_RuntimeObserverReceivesEvents(t *testing.T) {
|
||||||
provider := &llmHookTestProvider{}
|
provider := &llmHookTestProvider{}
|
||||||
al, agent, cleanup := newHookTestLoop(t, provider)
|
al, agent, cleanup := newHookTestLoop(t, provider)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
hook := &dualRuntimeObserverHook{
|
hook := &dualRuntimeObserverHook{
|
||||||
legacyCh: make(chan Event, 1),
|
|
||||||
runtimeCh: make(chan runtimeevents.Event, 1),
|
runtimeCh: make(chan runtimeevents.Event, 1),
|
||||||
}
|
}
|
||||||
if err := al.MountHook(NamedHook("runtime-observer", hook)); err != nil {
|
if err := al.MountHook(NamedHook("runtime-observer", hook)); err != nil {
|
||||||
|
|
@ -573,12 +561,6 @@ func TestAgentLoop_Hooks_RuntimeObserverPreferredOverLegacyObserver(t *testing.T
|
||||||
case <-time.After(2 * time.Second):
|
case <-time.After(2 * time.Second):
|
||||||
t.Fatal("timed out waiting for runtime observer event")
|
t.Fatal("timed out waiting for runtime observer event")
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
|
||||||
case evt := <-hook.legacyCh:
|
|
||||||
t.Fatalf("legacy observer unexpectedly received %v", evt.Kind)
|
|
||||||
case <-time.After(100 * time.Millisecond):
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAgentLoop_BtwCommand_UsesLLMHooks(t *testing.T) {
|
func TestAgentLoop_BtwCommand_UsesLLMHooks(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue