zoobzio January 14, 2025 Edit this page

Troubleshooting Guide

This guide covers common issues and their solutions when working with chit.

Errors

ErrUnknownResultType

chit: unknown result type

Cause: Your processor returned a Result that isn't *Response or *Yield.

Solution: Ensure your processor returns one of the valid result types:

// Correct
return &chit.Response{Content: "hello"}, nil
return &chit.Yield{Prompt: "question?", Continuation: ...}, nil

// Wrong - returning the interface, not a concrete type
var result chit.Result
return result, nil  // result is nil, causes ErrUnknownResultType

ErrNilProcessor

chit: processor is required

Cause: Passed nil as the processor to chit.New().

Solution: Always provide a valid processor:

// Wrong
chat := chit.New(nil, emitter)

// Correct
chat := chit.New(myProcessor, emitter)

ErrNilEmitter

chit: emitter is required

Cause: Passed nil as the emitter to chit.New().

Solution: Always provide a valid emitter:

// Wrong
chat := chit.New(processor, nil)

// Correct
chat := chit.New(processor, &MyEmitter{})

ErrEmitterClosed

chit: emitter is closed

Cause: Attempted to emit after calling Close() on the emitter.

Solution: Don't call Handle() after closing the emitter. If you need to reuse the chat, create a new emitter.

Common Issues

Continuation Not Being Called

Symptom: After yielding, the next input goes to the processor instead of the continuation.

Possible causes:

  1. New Chat instance: Each Chat has its own continuation state. If you're creating a new Chat for each request, the continuation is lost.
// Wrong - new chat each time
func handleRequest(input string) {
    chat := chit.New(processor, emitter)  // Fresh chat, no continuation
    chat.Handle(ctx, input)
}

// Correct - reuse the chat
var chat = chit.New(processor, emitter)
func handleRequest(input string) {
    chat.Handle(ctx, input)  // Same chat, continuation preserved
}
  1. Processor not returning Yield: Verify your processor actually returns a *Yield with a non-nil Continuation.
// Check with HasContinuation()
if chat.HasContinuation() {
    // Next Handle() will use continuation
}

EmitterFromContext Returns Nil

Symptom: chit.EmitterFromContext(ctx) returns nil inside your processor.

Possible causes:

  1. Context not from Handle: Chat injects the emitter into context before calling your processor. If you're calling the processor directly (e.g., in tests), the emitter won't be there.
// In tests, inject manually if needed
ctx := chit.WithEmitter(context.Background(), &helpers.CollectingEmitter{})
result, err := processor.Process(ctx, input, session)
  1. Context was replaced: If your processor creates a new context instead of deriving from the one passed in, the emitter is lost.
// Wrong
ctx := context.Background()  // New context, no emitter

// Correct
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)  // Derived, keeps emitter
defer cancel()

Session Not Updating

Symptom: Messages aren't appearing in the session after Handle().

Possible causes:

  1. Checking before Handle returns: Session is updated synchronously within Handle. If you're checking concurrently, you might read stale state.
  2. Using wrong session: If you provided a session via WithSession(), verify you're checking the same instance.
session := zyn.NewSession()
chat := chit.New(processor, emitter, chit.WithSession(session))

chat.Handle(ctx, "hello")

// Check the same session instance
messages := session.Messages()

Signals Not Firing

Symptom: Subscribed to signals but handler isn't called.

Possible causes:

  1. Subscribed after creation: Signal subscriptions must be active before the signal is emitted.
// Wrong - subscribe after New()
chat := chit.New(processor, emitter)
capitan.Subscribe(chit.ChatCreated, handler)  // Too late, already emitted

// Correct - subscribe before New()
capitan.Subscribe(chit.ChatCreated, handler)
chat := chit.New(processor, emitter)  // Handler called
  1. Subscription cancelled: Check if the cancel function was called.
cancel := capitan.Subscribe(chit.InputReceived, handler)
// Don't call cancel() until you're done listening

Race Conditions

Symptom: Sporadic test failures or inconsistent behavior under load.

Possible causes:

  1. Concurrent Handle calls: While Chat is thread-safe, concurrent calls serialize. If your processor has its own race conditions, they'll surface here.
  2. Shared mutable state in processor: Processors should be stateless or use proper synchronization.
// Wrong - shared state without synchronization
type MyProcessor struct {
    count int  // Race condition!
}

// Correct - use atomic or mutex
type MyProcessor struct {
    count atomic.Int64
}

Debugging Tips

Enable Signal Logging

Subscribe to all signals to trace the lifecycle:

signals := []capitan.Signal{
    chit.ChatCreated,
    chit.InputReceived,
    chit.ProcessingStarted,
    chit.ProcessingCompleted,
    chit.ProcessingFailed,
    chit.ResponseEmitted,
    chit.TurnYielded,
    chit.TurnResumed,
}

for _, sig := range signals {
    s := sig  // Capture for closure
    capitan.Subscribe(s, func(ctx context.Context, fields capitan.Fields) {
        log.Printf("[%s] %v", s.Name(), fields)
    })
}

Inspect Chat State

fmt.Printf("Chat ID: %s\n", chat.ID())
fmt.Printf("Has continuation: %v\n", chat.HasContinuation())
fmt.Printf("Session messages: %d\n", len(chat.Session().Messages()))
fmt.Printf("System prompt: %q\n", chat.Config().SystemPrompt)

Use CollectingEmitter for Debugging

emitter := &helpers.CollectingEmitter{}
chat := chit.New(processor, emitter)

chat.Handle(ctx, input)

fmt.Printf("Emitted messages: %d\n", len(emitter.Messages))
for i, msg := range emitter.Messages {
    fmt.Printf("  [%d] %s: %s\n", i, msg.Role, msg.Content)
}

fmt.Printf("Pushed resources: %d\n", len(emitter.Resources))
for i, res := range emitter.Resources {
    fmt.Printf("  [%d] %s: %s\n", i, res.Type, res.URI)
}

Next Steps