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:
- 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
}
- Processor not returning Yield: Verify your processor actually returns a
*Yieldwith a non-nilContinuation.
// 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:
- 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)
- 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:
- Checking before Handle returns: Session is updated synchronously within Handle. If you're checking concurrently, you might read stale state.
- 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:
- 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
- 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:
- Concurrent Handle calls: While Chat is thread-safe, concurrent calls serialize. If your processor has its own race conditions, they'll surface here.
- 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
- Testing Guide — test helpers and patterns
- Architecture — understand internals
- API Reference — function documentation