Using Session State

My demo program is a multi-agent application that accepts a topic BlogAgent, researches that topic (ResearcherAgent) and then drafts a blog post (AuthorAgent). The post is then reviewed by another agent (ReviewerAgent) who either accepts the draft or sends it back to the Author with requested changes.

I wanted to allow the user to refine the search after getting the draft. To do so, I needed short term memory. Here are the changes I made…

I implemented the search refinement flow by updating the research state and wiring it into the research step:

  • In ResearchState.cs, I added:
    • CurrentSubTask to represent the newest follow-up request
    • SearchRefinements to keep the history of prior refinement requests

SearchRefinements is a list<string>. As the ResearchState is created the code checks to see if there are refinements. If so the new refinements are appended to the existing request

       if (SearchRefinements.Count > 0)
        {
            string uniqueRefinements = string.Join(" | ", SearchRefinements
                .Where(r => !string.IsNullOrWhiteSpace(r))
                .Distinct(StringComparer.OrdinalIgnoreCase));

            if (!string.IsNullOrWhiteSpace(uniqueRefinements))
            {
                parts.Add($"Refinement history: {uniqueRefinements}");
            }
        }
  • BuildResearchQuery() to assemble a single, richer research prompt that includes:
    • the original task
    • the active follow-up refinement
    • previous refinement history
    • prior research findings/context
  • StartFollowUp() to preserve the existing draft/research state while resetting the review cycle and recording the previous subtask as a refinement
   public void StartFollowUp(string followUp)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(followUp);

        string trimmed = followUp.Trim();
        if (!string.IsNullOrWhiteSpace(CurrentSubTask) &&
            !string.Equals(CurrentSubTask, trimmed, StringComparison.OrdinalIgnoreCase))
        {
            SearchRefinements.Add(CurrentSubTask.Trim());
        }

        SearchRefinements.Add(trimmed);
        CurrentSubTask = trimmed;
        ReviewNotes = "";
        RevisionNumber = 0;
        NextStep = "";
    }
  • In ResearcherAgent.cs, I changed the research node to use state.BuildResearchQuery() for each research pass, so the agent searches with both the original goal and the refinement history instead of starting from scratch.
    public string BuildResearchQuery()
    {
        var parts = new List<string>();

        if (!string.IsNullOrWhiteSpace(MainTask))
        {
            parts.Add($"Original task: {MainTask.Trim()}");
        }

        if (!string.IsNullOrWhiteSpace(CurrentSubTask))
        {
            parts.Add($"Follow-up refinement: {CurrentSubTask.Trim()}");
        }

        if (SearchRefinements.Count > 0)
        {
            string uniqueRefinements = string.Join(" | ", SearchRefinements
                .Where(r => !string.IsNullOrWhiteSpace(r))
                .Distinct(StringComparer.OrdinalIgnoreCase));

            if (!string.IsNullOrWhiteSpace(uniqueRefinements))
            {
                parts.Add($"Refinement history: {uniqueRefinements}");
            }
        }

        if (ResearchFindings.Count > 0)
        {
            string context = string.Join("\n\n", ResearchFindings
                .Where(f => !string.IsNullOrWhiteSpace(f))
                .Select(f => f.Trim()));

            if (!string.IsNullOrWhiteSpace(context))
            {
                parts.Add($"Prior research context:\n{context}");
            }
        }

        return string.Join("\n\n", parts);
    }
  • In ResearchStateTests.cs, I added coverage confirming that:
    • a follow-up preserves the original task
    • the refinement is included in the generated search query
    • earlier research context is retained
   [Fact]
    public void StartFollowUp_PreservesDraftAndResearchButResetsReviewCycle()
    {
        var state = new ResearchState
        {
            MainTask = "topic",
            ResearchFindings = ["finding"],
            Draft = "draft",
            ReviewNotes = ResearchState.ApprovedMarker,
            RevisionNumber = ResearchState.MaxRevisions,
            NextStep = "END",
        };

        state.StartFollowUp("Add a caching section.");

        Assert.Equal("Add a caching section.", state.CurrentSubTask);
        Assert.Equal("draft", state.Draft);
        Assert.Equal(["finding"], state.ResearchFindings);
        Assert.Empty(state.ReviewNotes);
        Assert.Equal(0, state.RevisionNumber);
        Assert.Empty(state.NextStep);
    }

    [Fact]
    public void BuildResearchQuery_IncludesOriginalTopicAndFollowUpRefinement()
    {
        var state = new ResearchState
        {
            MainTask = "How to build a blog app",
            ResearchFindings = ["Concepts: context, prompts, and workflow.", "Drafting is done after research."],
            CurrentSubTask = "Add a caching section."
        };

        string query = state.BuildResearchQuery();

        Assert.Contains("How to build a blog app", query);
        Assert.Contains("Add a caching section.", query);
        Assert.Contains("Concepts: context, prompts, and workflow.", query);
        Assert.Contains("Drafting is done after research.", query);
    }

Why this enables refining a search

The crucial behavior is that a follow-up no longer discards the earlier work. The generated query now looks like:

  • Original task
  • Follow-up refinement
  • Prior refinement history
  • Earlier research findings

That means the next search is “narrowed and contextualized” rather than replaced, which is the actual refinement behavior you were asking for.

Note: The initial draft of this post was written by BlogWriter and then edited by Jesse Liberty
Illustrations by Copilot. Caution: LLMs make mistakes; this post is offered as is.

Unknown's avatar

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. Liberty is a Senior AI Engineer at the University of Pittsburgh Medical Center, and was a Team Lead and Senior Software Engineer for various corporations, a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a 21 year Microsoft MVP.
This entry was posted in AI, Essentials. Bookmark the permalink.