Keeping up with AI news can be a challenge.
Every day there’s a new model release, a new framework update, or some announcement that changes the landscape.
We find ourselves checking feeds, GitHub repositories, and blogs. It’s a bit of an effort.
What if we could build an AI agent that does this research for us?
An agent that scans multiple sources, aggregates the important stuff you interested in, and generates a polished newsletter we can read?
In this blog post, we’ll build exactly that using the Microsoft Agent Framework and the Background Responses feature.
We cover the following:
- What Are Background Responses
- Why Use Background Responses
- The Basics of Background Responses
- Creating an AI Researcher and Newsletter Publisher
- Core Features of the AINewsServiceAgent
- AINewsServiceAgent Process, Prompts, and Function Tools
- Bringing It All Together
- Calling the AINewsServiceAgent from a Console Application
- Implementing Background Responses for the AINewsServiceAgent
Our AI agent will search multiple news sources, monitor GitHub repositories for code updates, and generate a formatted markdown newsletter. All while keeping the UI responsive with a nice progress indicator.
A video demo and source code are included.
~
What Are Background Responses
When working with AI agents that need to perform multiple tool calls such as, searching news sources, fetching GitHub commits, or generating a newsletter, processing can take a while.
Traditional synchronous execution means your application sits there waiting, potentially timing out or leaving users staring at a frozen screen.
Background Responses solve this problem by allowing the agent to process requests asynchronously.
Instead of blocking until completion, the agent returns a continuation token that your application can use to poll for progress.
This is like how you might check on a long-running job in a queue-based system.
The OpenAIResponseClient makes this possible. Standard chat clients like GetChatClient run synchronously and do not support background processing.
Learn more about this here.
~
Why Use Background Responses
There are several reasons to use background responses for agent-based solutions.
- Non-blocking execution allows your application to remain responsive while the agent works. Crucial for long-running tasks that involve multiple tool calls, API requests, or complex reasoning
- Progress indication becomes possible because you can update the UI between polling intervals. Users can see that something is happening rather than wondering if the application has crashed.
- Timeout prevention Long-running synchronous requests can hit HTTP timeout limits. With background responses, each poll is a quick check rather than a sustained connection
In general, users appreciate knowing an operation is in progress and roughly how long it’s been running. Your agent can then report back to you via a communication channel of your choice when an operation has completed.
~
The Basics of Background Responses
The implementation pattern is straightforward. First, you create the agent using the OpenAI Responses client rather than the chat client.
#pragma warning disable OPENAI001 // OpenAI Responses API is in preview var responseClient = new OpenAIClient(apiKey) .GetOpenAIResponseClient(model); AIAgent agent = responseClient.CreateAIAgent( name: "AI News Digest Agent", instructions: "...", tools: [/* your tools */]);
Next, you enable background responses in the options:
AgentRunOptions options = new() { AllowBackgroundResponses = true };
Finally, you poll using the continuation token until the agent completes:
AgentRunResponse response = await agent.RunAsync(input, thread, options);
while (response.ContinuationToken is { } token)
{
await Task.Delay(TimeSpan.FromMilliseconds(200));
options.ContinuationToken = token;
response = await agent.RunAsync(thread, options);
}
// Agent has completed - ContinuationToken is null
Console.WriteLine($"Agent: {response.Text}");
options.ContinuationToken = null; // Reset for next request
The code within the while loop contains the continuation token.
~
Content Overload. More Signal. Less Noise.
The AI space moves incredibly fast. Microsoft updates the Agent Framework. Google announces new Gemini features Anthropic ships Claude improvements. Open AI releases and more.
Something every other week.\
If you’re a developer trying to stay current, you’re probably:
- Checking your trusted resources
- Watching GitHub repositories for new releases and commits
- Scanning tech news sites for announcements
- Trying to figure out what’s actually important versus what’s just noise
What we need is a personal AI research assistant that:
- Aggregates news from trusted sources
- Filters by relevance to specific companies or topics
- Tracks code repositories for meaningful updates
- Generates a readable digest we can review in minutes, not hours
In the next section, that’s exactly what we’ll build.
~
Creating an AI Researcher and Newsletter Publisher
Our solution consists of three main components:
- AINewsServiceAgent – The function tools that search various sources and generate the newsletter
- NewsItemModel – A simple data model for news items
- Program.cs – The console application that orchestrates the agent with background responses
Let’s walk through each component.
~
Core Features of the AINewsServiceAgent
The AINewsServiceAgent class provides four function tools that the AI agent can call:
| Tool | Description |
| SearchFoundryBlog() | Fetches the latest Microsoft AI Foundry blog posts |
| SearchAgentFrameworkUpdates() | Gets recent GitHub commits and releases from the Agent Framework repository |
| SearchCompanyNews(company) | Searches RSS feeds for company-specific AI news |
| GenerateNewsletter(title, content) | Saves the compiled newsletter to a markdown file |
The agent also exposes a status property that the console application can display during polling:
public static string CurrentStatus { get; private set; } = "Starting...";
public static void ResetStatus() => CurrentStatus = "Starting...";
~
AINewsServiceAgent Process
Here’s the flow when a user requests a newsletter digest:
Newsletter saved to: ai_digest_2026-01-24.md
Each tool call updates the CurrentStatus property. The console application displays this and keeps the user informed of what is happening.
~
AINewsServiceAgent Prompt
The agent’s instructions are crucial. They define the workflow, newsletter format, and quality expectations.
Here’s the complete prompt:
instructions: """
You are an AI news research agent that creates weekly digest newsletters.
Workflow:
Call SearchFoundryBlog() to get Microsoft AI Foundry updates
Call SearchAgentFrameworkUpdates() to get Microsoft Agent Framework code updates
Call SearchCompanyNews for each company: Microsoft, Google, Anthropic, OpenAI
Generate a markdown newsletter using GenerateNewsletter with ALL results
CRITICAL: When generating the newsletter content for GenerateNewsletter:
- You MUST include ALL headlines returned from ALL search functions
- For each news item, include: Headline, PublishedDate (if available), Summary, and Url
- IMPORTANT: Each headline MUST be paired with its MATCHING Url, Summary, and PublishedDate
- Do NOT mix up links - the Url for "Article A" must only appear with "Article A"
- Format URLs as markdown links: [Read more](Url)
- ALWAYS include the PublishedDate field when it is not empty
- Do NOT skip or omit any results - include everything returned
Newsletter format (use these exact section headers):
Brief executive summary (2-3 sentences about key themes)
"## Microsoft AI Foundry" section - ALL results from SearchFoundryBlog
"## Microsoft Agent Framework" section - ALL results from SearchAgentFrameworkUpdates
"## Microsoft" section - results from SearchCompanyNews("Microsoft")
"## Google" section - results from SearchCompanyNews("Google")
"## Anthropic" section - results from SearchCompanyNews("Anthropic")
"## OpenAI" section - results from SearchCompanyNews("OpenAI")
"## What to Watch" section with emerging trends
"## Predictions for the Next 90 Days" section - list 3-5 specific predictions
with confidence scores (Low/Medium/High)
Each story format:
**Headline** *(Jan 15, 2026)* - Summary [Read more](url)
Style:
- Professional, concise
- Include ALL source URLs as markdown links
- Do not invent or fabricate news - only use results from the search functions
- MUST have a blank line between each news item for proper formatting
A few things to note about this prompt:
- Explicit workflow ensures the agent calls tools in a predictable order
- CRITICAL sections prevent common LLM issues like mixing up URLs or skipping results
- Structured format produces consistent, readable newsletters
- Predictions with confidence adds analytical value beyond simple aggregation
~
AINewsServiceAgent Models
The NewsItemModel class is simple and captures essential fields for a news item:
public class NewsItemModel
{
public string Headline { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Source { get; set; } = string.Empty;
public string Company { get; set; } = string.Empty;
public string PublishedDate { get; set; } = string.Empty;
public NewsItemModel(string Headline, string Summary, string Url,
string Source, string Company, string PublishedDate = "")
{
this.Headline = Headline;
this.Summary = Summary;
this.Url = Url;
this.Source = Source;
this.Company = Company;
this.PublishedDate = PublishedDate;
}
}
The PublishedDate is stored as a formatted string (e.g., “Jan 15, 2026”) rather than a DateTime.
This simplifies the agent’s job when generating the newsletter – it can use the date string directly without formatting concerns.
~
AINewsServiceAgent Function Tools
Let’s examine each function tool in detail. When you supply a prompt, the AI agent automatically decides which of these to run.
SearchCompanyNews
This tool searches multiple RSS feeds for news about a specific company:
[Description("Searches for recent AI news and developments from a specific company.")]
public static async Task<List<NewsItemModel>> SearchCompanyNews(
[Description("The company to search for: Microsoft, Google, Anthropic, or OpenAI")]
string company)
{
CurrentStatus = $"Searching {company} news...";
var results = new List<NewsItemModel>();
var keywords = company.ToLower() switch
{
"microsoft" => new[] { "microsoft", "copilot", "azure", "bing" },
"google" => new[] { "google", "gemini", "deepmind", "bard" },
"anthropic" => new[] { "anthropic", "claude" },
"openai" => new[] { "openai", "chatgpt", "gpt-4", "gpt-5", "dall-e", "sora" },
_ => new[] { company.ToLower(), "ai", "artificial intelligence" }
};
foreach (var feedUrl in _rssFeeds)
{
var feedContent = await _httpClient.GetStringAsync(feedUrl);
var feedResults = ParseRssFeed(feedContent, company, keywords, feedUrl);
results.AddRange(feedResults);
}
return results.Take(5).ToList();
}
The keyword mapping ensures we catch related terms. For example, searching for Microsoft also finds articles about Copilot and Azure.
SearchFoundryBlog
This tool scrapes the Microsoft Foundry DevBlog for the latest posts:
[Description("Searches the Microsoft Foundry blog for the latest AI Foundry updates.")]
public static async Task<List<NewsItemModel>> SearchFoundryBlog()
{
CurrentStatus = "Fetching Microsoft Foundry blog...";
var results = new List<NewsItemModel>();
var htmlContent = await _httpClient.GetStringAsync(FoundryBlogUrl);
var keywords = new[] { "" }; // Match all articles - Foundry is all AI-related
results = ParseBlogPage(htmlContent, "Microsoft", keywords, FoundryBlogUrl);
return results.Take(10).ToList();
}
Since the Foundry blog is entirely AI-focused, we don’t filter by keywords – every article is relevant.
SearchAgentFrameworkUpdates
This tool fetches recent commits and releases from the Agent Framework GitHub repository:
[Description("Searches the Microsoft Agent Framework GitHub repository for recent code updates.")]
public static async Task<List<NewsItemModel>> SearchAgentFrameworkUpdates()
{
CurrentStatus = "Fetching GitHub Agent Framework updates...";
return await FetchGitHubUpdates("microsoft", "agent-framework", "Microsoft");
}
The FetchGitHubUpdates helper method queries the GitHub API for commits from the last 7 days, focusing on significant changes like features, examples, and releases.
GenerateNewsletter
This tool saves the compiled newsletter to a markdown file:
[Description("Generates a markdown newsletter digest and saves it to file.")]
public static string GenerateNewsletter( [Description("The newsletter title")] string title, [Description("The full markdown content of the newsletter")] string markdownContent)
{
CurrentStatus = "Generating newsletter...";
var filename = $"ai_digest_{DateTime.Now:yyyy-MM-dd}.md";
var header = $"""
# {title}
**Generated:** {DateTime.Now:dddd, MMMM d, yyyy 'at' h:mm tt}
---
""";
var fullContent = header + markdownContent;
File.WriteAllText(filename, fullContent);
SaveLastRunTime();
return $"Newsletter saved to {filename}";
}
~
AINewsServiceAgent Helper Methods
Several helper methods support the function tools.
ParseRssFeed
This method handles both RSS 2.0 and Atom feed formats:
private static List<NewsItemModel> ParseRssFeed(string feedContent, string company, string[] keywords, string feedUrl)
{
var results = new List<NewsItemModel>();
var doc = XDocument.Parse(feedContent);
XNamespace atom = "http://www.w3.org/2005/Atom";
// Handle both RSS 2.0 and Atom feeds
var items = doc.Descendants("item").ToList();
var isAtomFeed = false;
if (!items.Any())
{
items = doc.Descendants(atom + "entry").ToList();
isAtomFeed = true;
}
foreach (var item in items)
{
var title = GetElementValue(item, "title") ?? "";
var description = GetElementValue(item, "description")
?? GetElementValue(item, "summary")
?? GetElementValue(item, "content")
?? "";
// Extract link - different for Atom vs RSS
var link = isAtomFeed
? item.Elements(atom + "link")
.FirstOrDefault(e => e.Attribute("rel")?.Value == "alternate")
?.Attribute("href")?.Value ?? ""
: GetElementValue(item, "link") ?? "";
var pubDateStr = GetElementValue(item, "pubDate")
?? GetElementValue(item, "published")
?? GetElementValue(item, "updated")
?? "";
var publishedDate = FormatPublishedDate(pubDateStr);
var searchText = $"{title} {description}".ToLower();
if (keywords.Any(k => searchText.Contains(k)))
{
// Clean up and add to results
description = Regex.Replace(description, "<[^>]+>", "");
if (description.Length > 300)
description = description[..300] + "...";
results.Add(new NewsItemModel(
Headline: title,
Summary: description,
Source: GetSourceName(feedUrl),
Url: link,
Company: company,
PublishedDate: publishedDate
));
}
}
return results;
}
FetchGitHubUpdates
This method queries the GitHub API for recent repository activity:
private static async Task<List<NewsItemModel>> FetchGitHubUpdates(
string owner, string repo, string company)
{
var results = new List<NewsItemModel>();
var since = DateTime.UtcNow.AddDays(-7);
using var request = new HttpRequestMessage(HttpMethod.Get,
$"https://api.github.com/repos/{owner}/{repo}/commits?since={since:yyyy-MM-ddTHH:mm:ssZ}&per_page=20");
request.Headers.Add("User-Agent", "AI-News-Agent");
request.Headers.Add("Accept", "application/vnd.github.v3+json");
if (!string.IsNullOrEmpty(_githubToken))
request.Headers.Add("Authorization", $"Bearer {_githubToken}");
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode) return results;
var commits = await response.Content.ReadFromJsonAsync<JsonElement>();
foreach (var commit in commits.EnumerateArray())
{
var message = commit.GetProperty("commit")
.GetProperty("message").GetString() ?? "";
var firstLine = message.Split('\n')[0];
var url = commit.GetProperty("html_url").GetString() ?? "";
// Filter for significant commits
var lowerMessage = firstLine.ToLower();
if (lowerMessage.Contains("feat") || lowerMessage.Contains("add") ||
lowerMessage.Contains("new") || lowerMessage.Contains("example"))
{
results.Add(new NewsItemModel(
Headline: $"[Code] {firstLine}",
Summary: $"Update in {owner}/{repo}",
Source: "GitHub",
Url: url,
Company: company,
PublishedDate: FormatPublishedDate(dateStr)
));
}
}
// Also check for releases
await FetchGitHubReleases(owner, repo, company, since, results);
return results;
}
The method filters commits to focus on features, additions, and examples rather than noise like documentation typos or merge commits.
~
Bringing It All Together
The Program.cs file wires everything up. Here’s the main structure:
static async Task Main(string[] args)
{
// Create the OpenAI Responses client (supports background responses)
var responseClient = new OpenAIClient(apiKey)
.GetOpenAIResponseClient(model);
AIAgent agent = responseClient.CreateAIAgent(
name: "AI News Digest Agent",
instructions: """
You are an AI news research agent that creates weekly digest newsletters.
Workflow:
1. Call SearchFoundryBlog() to get Microsoft AI Foundry updates
2. Call SearchAgentFrameworkUpdates() to get Microsoft Agent Framework code updates
3. Call SearchCompanyNews for each company: Microsoft, Google, Anthropic, OpenAI
4. Generate a markdown newsletter using GenerateNewsletter with ALL results
CRITICAL: When generating the newsletter content for GenerateNewsletter:
- You MUST include ALL headlines returned from ALL search functions
- For each news item, include: Headline, PublishedDate (if available), Summary, and Url
- IMPORTANT: Each headline MUST be paired with its MATCHING Url, Summary, and PublishedDate from the same result
- Do NOT mix up links - the Url for "Article A" must only appear with "Article A"
- Format URLs as markdown links: [Read more](Url)
- ALWAYS include the PublishedDate field when it is not empty
- Do NOT skip or omit any results - include everything returned
Newsletter format (use these exact section headers):
1. Brief executive summary (2-3 sentences about key themes)
2. "## Microsoft AI Foundry" section - ALL results from SearchFoundryBlog
3. "## Microsoft Agent Framework" section - ALL results from SearchAgentFrameworkUpdates. Only C# and .NET. Do not include Python code.
4. "## Microsoft" section - results from SearchCompanyNews("Microsoft")
5. "## Google" section - results from SearchCompanyNews("Google")
6. "## Anthropic" section - results from SearchCompanyNews("Anthropic")
7. "## OpenAI" section - results from SearchCompanyNews("OpenAI")
8. "## What to Watch" section with emerging trends
9. "## Predictions for the Next 90 Days" section - REQUIRED: Based on the news gathered, list 3-5 specific predictions about what might happen in AI over the next 90 days. For EACH prediction, include a confidence score (Low/Medium/High) based on the strength of evidence from the news.
Format each prediction as:
- **Prediction:** [Your prediction] **Confidence:** [Low/Medium/High] - [Brief reasoning for the confidence level]
Each story format (IMPORTANT - include blank line after each item):
WITH DATE (when PublishedDate field is not empty):
**Headline** *(Jan 15, 2026)* - Summary [Read more](url)
WITHOUT DATE (when PublishedDate field is empty):
**Headline** - Summary [Read more](url)
RULES:
- Check each item's PublishedDate field - if it has a value like "Jan 15, 2026", include it in italics
- The blank line between items is CRITICAL for proper markdown rendering
- Copy the PublishedDate exactly as provided in the search results
Style:
- Professional, concise
- Include ALL source URLs as markdown links
- Do not invent or fabricate news - only use results from the search functions
- MUST have a blank line between each news item for proper formatting
""",
tools: [
AIFunctionFactory.Create(AINewsServiceAgent.SearchFoundryBlog),
AIFunctionFactory.Create(AINewsServiceAgent.SearchAgentFrameworkUpdates),
AIFunctionFactory.Create(AINewsServiceAgent.SearchCompanyNews),
AIFunctionFactory.Create(AINewsServiceAgent.GenerateNewsletter)
]);
AgentRunOptions options = new() { AllowBackgroundResponses = true };
AgentThread thread = agent.GetNewThread();
Console.WriteLine("=== AI News Digest Agent ===");
Console.WriteLine("Commands:");
Console.WriteLine(" 'Generate this week's digest'");
Console.WriteLine(" 'What's new from Anthropic?'");
Console.WriteLine(" 'Create a newsletter focused on AI agents'");
Console.WriteLine();
while (true)
{
Console.Write("You: ");
string? input = Console.ReadLine();
if (string.IsNullOrEmpty(input)) break;
var stopwatch = Stopwatch.StartNew();
_progressPosition = 0;
AINewsServiceAgent.ResetStatus();
// Initial call - may return with continuation token if still processing
AgentRunResponse response = await agent.RunAsync(input, thread, options);
// Poll with continuation token until complete (framework's background responses pattern)
while (response.ContinuationToken is { } token)
{
UpdateProgressBar(stopwatch);
await Task.Delay(TimeSpan.FromMilliseconds(200)); // Poll every 1 seconds
options.ContinuationToken = token;
response = await agent.RunAsync(thread, options);
}
stopwatch.Stop();
ClearProgressBar();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Completed in {stopwatch.Elapsed:mm\\:ss}");
Console.ResetColor();
Console.WriteLine();
Console.WriteLine($"Agent: {response.Text}");
Console.WriteLine();
options.ContinuationToken = null;
}
}
You’ll see in the above code there is a method UpdateProgressBar method. Lets look at that in more detail.
~
Implementing a Progress bar for the AINewsServiceAgent
The key to a good user experience is the progress bar. Here’s how it’s implemented:
private static int _progressPosition = 0;
private static readonly int _progressWidth = 30;
private static void UpdateProgressBar(Stopwatch stopwatch)
{
// Create animated progress bar
_progressPosition = (_progressPosition + 1) % (_progressWidth * 2);
var barPosition = _progressPosition < _progressWidth
? _progressPosition
: (_progressWidth * 2) - _progressPosition;
var bar = new string(' ', _progressWidth);
var barChars = bar.ToCharArray();
// Create a 3-character moving block
for (int i = 0; i < 3; i++)
{
var pos = (barPosition + i) % _progressWidth;
if (pos < _progressWidth) barChars[pos] = '█';
}
// Get current status from agent
var status = AINewsServiceAgent.CurrentStatus;
if (status.Length > 45) status = status[..42] + "...";
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write($"\rResearching [{new string(barChars)}] {stopwatch.Elapsed:mm\\:ss}");
Console.ResetColor();
Console.Write($"\n\r{status,-50}");
Console.SetCursorPosition(0, Console.CursorTop - 1);
}
This creates an animated block that bounces back and forth while displaying:
- Elapsed time in
mm:ssformat - Current operation status (e.g., “Searching Microsoft news…”)
The result looks like this during execution:
Perfect.
~
Demo
Here’s a video showing the AI need agent in action:
TODO – record video
When complete, the output looks like:
Completed in 01:23 Agent: I've generated this week's AI news digest. The newsletter has been saved to ai_digest_2026-01-21.md and includes updates from Microsoft AI Foundry, the Agent Framework repository, and general AI news from Microsoft, Google, Anthropic, and OpenAI. Key highlights this week: - Microsoft released new Agent Framework features for memory persistence - Google announced Gemini 2.0 improvements - Anthropic shipped Claude performance updates - OpenAI continued GPT-5 development
The newsletter includes predictions for the next 90 days with confidence scores.
The generated newsletter is a well-formatted markdown file with all sections, links, and dates properly attributed.
~
Summary
In this post, we built an AI-powered research agent that:
- Uses Background Responses to handle long-running multi-tool workflows
- Aggregates news from RSS feeds, blog pages, and GitHub repositories
- Generates formatted newsletters with proper attribution and links
- Provides real-time progress indication during processing
- Includes predictions with confidence scores for added analytical value
You can adapt this pattern for any agent-based application where tool calls might take significant time.
This might be – document processing, data analysis, complex reasoning tasks, or any workflow involving multiple external API calls.
~
Further Reading
~
Enjoy what you’ve read, have questions about this content, or would like to see another topic?
Drop me a note below.
You can schedule a call using my Calendly link to discuss consulting and development services.


Agent.NET has evolved significantly since the alpha.1 announcement — alpha.2 and now alpha.3 bring proper MAF execution, symmetrical InProcess/Durable workflows, and a more cohesive workflow CE with ROP built in.Full release history:github.com/JordanMarr/A…#fsharp #dotnet #aiagents
