zoobzio December 13, 2025 Edit this page

Multi-Turn Conversations

Patterns for building complex workflows using session context.

Customer Support Agent

Multi-step support workflow with context:

package main

import (
    "context"
    "fmt"

    "github.com/zoobz-io/zyn"
)

type SupportAgent struct {
    extractor  *zyn.ExtractionSynapse[CustomerIssue]
    classifier *zyn.ClassificationSynapse
    responder  *zyn.TransformSynapse
    resolver   *zyn.BinarySynapse
}

type CustomerIssue struct {
    Name        string `json:"name"`
    Issue       string `json:"issue"`
    ProductArea string `json:"product_area"`
}

func (c CustomerIssue) Validate() error {
    if c.Issue == "" {
        return fmt.Errorf("issue description required")
    }
    return nil
}

func NewSupportAgent(provider zyn.Provider) (*SupportAgent, error) {
    extractor, _ := zyn.Extract[CustomerIssue]("Extract customer name, issue, and product area", provider)
    classifier, _ := zyn.Classification("Issue severity", []string{"low", "medium", "high", "critical"}, provider)
    responder, _ := zyn.Transform("Generate empathetic customer service response", provider)
    resolver, _ := zyn.Binary("Can this issue be resolved with available information?", provider)

    return &SupportAgent{
        extractor:  extractor,
        classifier: classifier,
        responder:  responder,
        resolver:   resolver,
    }, nil
}

type SupportResponse struct {
    CustomerName string
    Severity     string
    Response     string
    NeedsHuman   bool
}

func (a *SupportAgent) HandleTicket(ctx context.Context, customerMessage string) (*SupportResponse, error) {
    // Shared session for full context
    session := zyn.NewSession()

    // Step 1: Extract issue details
    issue, err := a.extractor.Fire(ctx, session, customerMessage)
    if err != nil {
        return nil, fmt.Errorf("extraction failed: %w", err)
    }

    // Step 2: Classify severity (sees extraction context)
    severity, err := a.classifier.Fire(ctx, session, issue.Issue)
    if err != nil {
        return nil, fmt.Errorf("classification failed: %w", err)
    }

    // Step 3: Check if resolvable (sees full context)
    resolvable, err := a.resolver.Fire(ctx, session, "Based on the issue and severity, can this be resolved automatically?")
    if err != nil {
        return nil, fmt.Errorf("resolution check failed: %w", err)
    }

    // Step 4: Generate response (sees all previous context)
    prompt := fmt.Sprintf("Respond to %s about their %s severity issue regarding %s",
        issue.Name, severity, issue.ProductArea)
    response, err := a.responder.Fire(ctx, session, prompt)
    if err != nil {
        return nil, fmt.Errorf("response generation failed: %w", err)
    }

    return &SupportResponse{
        CustomerName: issue.Name,
        Severity:     severity,
        Response:     response,
        NeedsHuman:   !resolvable || severity == "critical",
    }, nil
}

Interview Assistant

Conduct a multi-question interview:

type InterviewAssistant struct {
    questioner *zyn.TransformSynapse
    evaluator  *zyn.ClassificationSynapse
    summarizer *zyn.TransformSynapse
}

type InterviewResult struct {
    Questions   []string
    Answers     []string
    Evaluations []string
    Summary     string
}

func (a *InterviewAssistant) Conduct(ctx context.Context, topic string, numQuestions int) (*InterviewResult, error) {
    session := zyn.NewSession()
    result := &InterviewResult{}

    // Initialize context
    initPrompt := fmt.Sprintf("You are conducting an interview about %s. Ask probing questions.", topic)
    session.Append(zyn.RoleSystem, initPrompt)

    for i := 0; i < numQuestions; i++ {
        // Generate question based on previous context
        question, err := a.questioner.Fire(ctx, session, fmt.Sprintf("Ask question %d of %d", i+1, numQuestions))
        if err != nil {
            return nil, err
        }
        result.Questions = append(result.Questions, question)

        // Simulate getting answer (in real app, this comes from user)
        answer := getAnswer(question)  // placeholder
        result.Answers = append(result.Answers, answer)

        // Add answer to session for context
        session.Append(zyn.RoleUser, answer)

        // Evaluate answer
        evaluation, _ := a.evaluator.Fire(ctx, session, "Evaluate the quality of the last answer")
        result.Evaluations = append(result.Evaluations, evaluation)
    }

    // Final summary with full context
    result.Summary, _ = a.summarizer.Fire(ctx, session, "Summarize the interview and key findings")

    return result, nil
}

Iterative Refinement

Refine output through multiple iterations:

type RefiningWriter struct {
    writer   *zyn.TransformSynapse
    critic   *zyn.TransformSynapse
    improver *zyn.TransformSynapse
}

func (r *RefiningWriter) Write(ctx context.Context, prompt string, iterations int) (string, error) {
    session := zyn.NewSession()

    // Initial draft
    draft, err := r.writer.Fire(ctx, session, prompt)
    if err != nil {
        return "", err
    }

    for i := 0; i < iterations; i++ {
        // Get critique (sees previous drafts)
        critique, err := r.critic.Fire(ctx, session, fmt.Sprintf("Critique this draft: %s", draft))
        if err != nil {
            return draft, nil  // Return current draft on error
        }

        // Improve based on critique (sees draft and critique)
        improved, err := r.improver.Fire(ctx, session, fmt.Sprintf("Improve the draft based on this feedback: %s", critique))
        if err != nil {
            return draft, nil
        }

        draft = improved
    }

    return draft, nil
}

Branching Conversations

Handle different paths based on responses:

type DecisionTree struct {
    decider   *zyn.ClassificationSynapse
    handlers  map[string]*zyn.TransformSynapse
}

func (d *DecisionTree) Process(ctx context.Context, input string) (string, error) {
    session := zyn.NewSession()

    // Decide which branch to take
    decision, err := d.decider.Fire(ctx, session, input)
    if err != nil {
        return "", err
    }

    // Get appropriate handler
    handler, ok := d.handlers[decision]
    if !ok {
        return "", fmt.Errorf("no handler for decision: %s", decision)
    }

    // Process with context from decision
    return handler.Fire(ctx, session, "Process based on the decision")
}

Context Window Management

Handle long conversations:

type ConversationManager struct {
    synapse    *zyn.TransformSynapse
    summarizer *zyn.TransformSynapse
    maxMessages int
}

func (m *ConversationManager) Chat(ctx context.Context, session *zyn.Session, userMessage string) (string, error) {
    // Check if we need to compress history
    if session.Len() > m.maxMessages {
        if err := m.compressHistory(ctx, session); err != nil {
            return "", err
        }
    }

    return m.synapse.Fire(ctx, session, userMessage)
}

func (m *ConversationManager) compressHistory(ctx context.Context, session *zyn.Session) error {
    // Get messages to summarize (keep last few)
    messages := session.Messages()
    toSummarize := messages[:len(messages)-4]  // Keep last 4

    // Create summary
    summarySession := zyn.NewSession()
    summarySession.SetMessages(toSummarize)
    summary, err := m.summarizer.Fire(ctx, summarySession, "Summarize this conversation")
    if err != nil {
        return err
    }

    // Replace with summary + recent messages
    session.SetMessages(append(
        []zyn.Message{{Role: zyn.RoleSystem, Content: "Previous context: " + summary}},
        messages[len(messages)-4:]...,
    ))

    return nil
}

Parallel Processing with Shared Context

Process multiple aspects in parallel, then combine:

type ParallelAnalyzer struct {
    sentiment  *zyn.SentimentSynapse
    topics     *zyn.ExtractionSynapse[Topics]
    entities   *zyn.ExtractionSynapse[Entities]
    summarizer *zyn.TransformSynapse
}

type AnalysisResult struct {
    Sentiment string
    Topics    []string
    Entities  []string
    Summary   string
}

func (a *ParallelAnalyzer) Analyze(ctx context.Context, text string) (*AnalysisResult, error) {
    var wg sync.WaitGroup
    var sentiment zyn.SentimentResponse
    var topics Topics
    var entities Entities
    var errs []error
    var mu sync.Mutex

    // Parallel analysis with independent sessions
    wg.Add(3)

    go func() {
        defer wg.Done()
        s := zyn.NewSession()
        result, err := a.sentiment.FireWithDetails(ctx, s, text)
        mu.Lock()
        if err != nil {
            errs = append(errs, err)
        } else {
            sentiment = result
        }
        mu.Unlock()
    }()

    go func() {
        defer wg.Done()
        s := zyn.NewSession()
        result, err := a.topics.Fire(ctx, s, text)
        mu.Lock()
        if err != nil {
            errs = append(errs, err)
        } else {
            topics = result
        }
        mu.Unlock()
    }()

    go func() {
        defer wg.Done()
        s := zyn.NewSession()
        result, err := a.entities.Fire(ctx, s, text)
        mu.Lock()
        if err != nil {
            errs = append(errs, err)
        } else {
            entities = result
        }
        mu.Unlock()
    }()

    wg.Wait()

    if len(errs) > 0 {
        return nil, fmt.Errorf("analysis errors: %v", errs)
    }

    // Combine results with summarizer (new session with combined context)
    combineSession := zyn.NewSession()
    combinedContext := fmt.Sprintf("Sentiment: %s\nTopics: %v\nEntities: %v",
        sentiment.Overall, topics.Items, entities.Items)

    summary, _ := a.summarizer.Fire(ctx, combineSession, "Summarize these analysis results: "+combinedContext)

    return &AnalysisResult{
        Sentiment: sentiment.Overall,
        Topics:    topics.Items,
        Entities:  entities.Items,
        Summary:   summary,
    }, nil
}

Next Steps