Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
156131 stories
·
33 followers

Every new iOS 27 feature that’s worth knowing about

1 Share
While it's not flashy like Apple’s new Siri AI and Apple Intelligence upgrades, there are still a number of additions to iOS 27 worth looking at.
Read the whole story
alvinashcraft
9 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Migrating Agentic Code Python -> C# Part 2

1 Share

In Part 1 of this multi-part series, I laid out my goal to migrate the Python agentics program from the previous series to C#. To do this migration I’m going to work my way down through my Python script and refactor it breaking out classes and refactoring to use Microsoft Agent Framework.

Note: to make sense of this code, you’ll want to start with the Python example. The code for that begins here.

We begin with bringing in the config.json file. We’ll use the identical file, and bring it into Program.cs

const string fileName = "config.json";

using var stream = File.OpenRead(fileName);
using var document = JsonDocument.Parse(stream);
JsonElement config = document.RootElement;

string? GetValue(string key) =>
    config.TryGetProperty(key, out JsonElement value) ? value.GetString() : null;

Environment.SetEnvironmentVariable("OPENAI_API_KEY", GetValue("API_KEY"));
Environment.SetEnvironmentVariable("OPENAI_BASE_URL", GetValue("OPENAI_API_BASE"));
Environment.SetEnvironmentVariable("TAVILY_API_KEY", GetValue("TAVILY_API_KEY"));

string modelName = "gpt-4o-mini";

var openAIClient = new OpenAIClient(
    new ApiKeyCredential(Environment.GetEnvironmentVariable("OPENAI_API_KEY")!),
    new OpenAIClientOptions
    {
        Endpoint = new Uri(Environment.GetEnvironmentVariable("OPENAI_BASE_URL")!)
    });

Next, we set up the Tavily search tool:

var tavilyHttpClient = new HttpClient { BaseAddress = new Uri("https://api.tavily.com/") };
tavilyHttpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TAVILY_API_KEY"));

AIFunction tavilyTool = AIFunctionFactory.Create(
    async (string query) =>
    {
        var request = new
        {
            query,
            max_results = 5,
            topic = "general",
            include_answer = false,
            include_raw_content = false,
            search_depth = "basic"
        };

        using HttpResponseMessage response = await tavilyHttpClient.PostAsJsonAsync("search", request);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    },
    name: "tavily_search",
    description: "A search engine optimized for comprehensive, accurate, and trusted results.");

A little more verbose, but we’re doing a bit of extra work along the way. Let’s go down to where we create the ResearchState. To do that, we’ll create ResearchState.cs:

namespace BlogMigration;

/// <summary>State for the research workflow.</summary>
public class ResearchState
{
    public string MainTask { get; set; } = "";
    public List<string> ResearchFindings { get; set; } = [];
    public string Draft { get; set; } = "";
    public string ReviewNotes { get; set; } = "";
    public int RevisionNumber { get; set; }
    public string NextStep { get; set; } = "";
    public string CurrentSubTask { get; set; } = "";
}

In the next blog post we’ll create the first of our agents: Blogger. We’ll mimic BloggerChain, passing in the llm (IChatClient) and the chat options.

Read the whole story
alvinashcraft
10 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Message pumps fail in the transaction details

1 Share

TL;DR: The hard part of a message pump is not reading bytes from a queue. The hard part is deciding what happens when message handling writes to a database, sends more messages, fails halfway through, moves to another broker, or runs in a cloud service with different transaction semantics. That is where a small infrastructure project becomes a platform commitment.

The funny part is that none of this looked scary at the beginning.

It looked like a loop. Read from a queue. Deserialize a message. Find the handler. Call it. Add bounded concurrency when load arrives. Add graceful shutdown when deployments get rough.

Then the real requirements appear.

Every message needs logging. Failed messages need exception details. Successful messages need auditing. Handlers need dependency injection scopes. Some operations must run in a transaction. Some messages produce more messages. The code that looked like a loop starts to look like middleware.

That is the moment to be careful. You are no longer writing a helper. You are defining the failure behavior of the system.

The loop grew middleware

Business handlers rarely run alone. They need cross-cutting behavior around them. That behavior starts with logging and error handling, but it usually grows into auditing, tracing, metrics, retries, dependency injection scopes, and transaction boundaries.

The sample repository has a deliberately compact “how it probably looks like in reality” version. It creates a transaction, reads the message inside that transaction, deserializes the payload, creates a child service provider, builds middleware, runs the middleware, completes the transaction, and releases the concurrency slot.

The complete version with middleware is in ThePumpHowItProbablyLooksLikeInReality.cs.

async Task FetchAndHandleAndReleaseWithMiddleware(SemaphoreSlim semaphore, CancellationToken token = default) {
    using (var transaction = CreateTransaction()) {
        var (payload, headers) = await ReadFromQueue(token, transaction).ConfigureAwait(false);
        var message = Deserialize(payload, headers);

        using (var childServiceProvider = CreateChildServiceProvider()) {
            try {
                var middlewareFuncs = new Func<HandlerContext, Func<HandlerContext, CancellationToken, Task>, CancellationToken, Task>[] { Middleware1, Middleware2 };
                var middleware = FlextensibleMiddleware(childServiceProvider, middlewareFuncs, token);

                await middleware(message).ConfigureAwait(false);

                transaction.Complete();
            }
            catch (Exception) {
                // Just log?
            }
            finally {
                semaphore.Release();
            }
        }
    }
}

The comment in the catch block is doing a lot of work. “Just log?” is not an implementation detail. It is a policy decision. Should the message be retried? Moved to an error queue? Acknowledged and skipped? Rolled back? How many times? With which delay? Who gets alerted?

Those answers belong to the message pump because the pump owns the boundary between broker state and application side effects.

The transaction boundary bites back

In the original system, Microsoft Message Queuing and SQL Server could participate in distributed transactions through the Distributed Transaction Coordinator. With a transaction scope and a two-phase commit protocol, the system could coordinate the receive, database work, and outgoing sends.

Message handling transaction boundary and failure points A message handling flow from receive through business logic to database write and outgoing messages, with failure markers between operations. Receivebroker message Business logichandler code Databasestate change Outgoingnew messages ! ! ! The hard question is which effects commit together, which can be duplicated, and which can escape early.
Message handling gets difficult at the boundaries between receive, state changes, and outgoing messages.

That gives a strong guarantee. Either the receive, database writes, and outgoing messages commit together, or they do not commit.

It also adds coordination cost. The transaction manager has to talk to the resources involved, and each resource has to agree on the outcome. That extra round trip and coordination can reduce throughput sharply.

This is the first trade-off many teams hit. They want the safety of a coordinated transaction and the speed of a simpler broker operation. The broker choice decides how much of that combination is available.

Receive-only changes the story

Some brokers do not coordinate the receive, database update, and outgoing sends into one transaction. RabbitMQ, Amazon Simple Queue Service, and Azure Storage Queues are common examples of systems where the receive side has its own acknowledgement model.

Receive-only transaction boundary A receive-only transaction covers broker acknowledgement, while database writes and outgoing messages sit outside that boundary and can succeed or fail independently. Receive transactionreceive + ack/nack Broker message Databaseoutside boundary Outgoing messagesoutside boundary If handling fails after the database write,retry can repeat the business effect. If a send escapes early, downstream codecan see a partial operation.
With receive-only transactions, broker acknowledgement is protected separately from application side effects.

That model can be perfectly valid. It is also different.

Suppose a handler receives a message, writes to the database, and then fails before acknowledging the broker message. The broker redelivers the message later. The database write already happened. If the handler is not idempotent, the second attempt can create the same business effect twice.

The reverse can also happen. The handler sends a message to one destination, then fails before finishing the rest of the work. A downstream consumer can observe a message that represents work the original handler did not fully complete.

People often call those ghost messages. The name is informal, but the risk is real: a message escapes from a business operation that later rolls back or fails.

Cloud queues bring their own rules

Cloud brokers add another layer of semantics. Some queues use a visibility timeout. When the pump receives a message, the broker hides that message for a period. If the handler does not finish and delete the message before the timeout expires, the broker can make it visible again.

Visibility timeout renewal during message processing A cloud queue hides a received message for a visibility timeout. The pump must renew the timeout while long-running business logic continues, or the message can be delivered again. Queuemessage hidden Message pumphandler running Renew visibility timeout Timeout expires: redelivery Long-running work changes the receive contract.
Visibility timeouts make the pump responsible for renewing the broker lease while handler work continues.

That means long-running handlers need renewal logic. The pump may have to extend the visibility timeout while business code is still running. Renew too aggressively and failures take longer to recover. Renew too little and duplicate processing appears while the first handler is still working.

Other brokers provide features that help with outgoing messages. Azure Service Bus, for example, has transaction support for sending messages through the incoming entity by using send-via behavior. That can reduce ghost-message risk for outgoing broker messages.

It does not make your database write magically part of the same broker operation. You still need idempotent business logic where your handler changes application state.

Cost sneaks into the loop

On-premises queues often make teams think in throughput, disk, memory, and operations work. Cloud queues add a direct cost model. Sends, receives, renewals, management calls, topic operations, and connections can all show up in billing or capacity limits depending on the provider and tier.

The exact prices change, so the pump should not bake in assumptions from a slide or a blog post. But the design pressure is stable: every broker operation has latency, and many broker operations have cost.

Batching becomes a design concern. A loop that sends one outgoing message per broker call is easier to write, but it pays latency and operation cost for every send. A batching sender can group messages, but then it has to respect payload limits, destinations, transaction rules, and failure behavior.

That batching logic becomes transport-specific fast. The rules for one broker are not the rules for another.

Pub/sub adds topology

Sending commands to a known queue is the simpler case. Publish/subscribe adds topology.

Publish/subscribe topology with topic and subscriptions A publisher sends one event to a topic. The broker routes copies through subscriptions to multiple subscriber queues, adding topology that must be configured, monitored, and migrated. Publisherevent Topicrouting point Subscription A Subscription B Subscription C Queue A Queue B Queue C
Publish/subscribe turns a single send path into broker topology that has to be owned over time: routing, subscriptions, ownership, and migration rules.

Some brokers have topics and subscriptions as first-class concepts. Some have only queues and require another service, table, or convention to map a published event to subscribers. Once the pump supports publish/subscribe, it also needs routing rules, subscription storage, topology deployment, and a way to evolve those rules without losing messages.

That is not only code. It is documentation and operations. Someone has to know which event goes where, who owns the subscription, what happens during deployment, how to monitor dead-letter queues, and how to answer the uncomfortable question: can production handle the next customer?

The pump became the platform.

A note on perspective: The events described in this post happened before I joined Particular Software. Today, I work on NServiceBus, so I have certainly developed opinions about the operational costs of owning messaging infrastructure. Building a message pump myself was still one of the most educational experiences of my career. I do not regret doing it. It simply gave me a better appreciation for the trade-offs involved when deciding whether to build or adopt an existing solution.

Build or use what works?

Writing a message pump taught me more about distributed systems than most books I had read at the time. I would still recommend building one as an exercise, even if you never intend to run it in production. Build a small one against your favorite broker. Make it read, deserialize, dispatch, retry, limit concurrency, shut down cleanly, and publish a follow-up message.

Then break it.

Kill the process during a handler. Drop the database connection after the write but before acknowledgement. Send a batch that exceeds the broker payload limit. Let a visibility timeout expire. Add a second subscriber. Watch what happens.

The question eventually becomes whether messaging infrastructure differentiates your business. Most teams benefit from using proven solutions for common infrastructure problems and focusing their effort elsewhere. That is the argument behind Use What Works, and message pumps fit it well. The first loop is easy. The years of edge cases, operational behavior, documentation, and support are the expensive part.

After that, the make-or-buy decision becomes more honest. If your business needs unusual semantics and your team can own the complexity for years, building may be reasonable. If you need retries, routing, topology, observability, auditing, error queues, transport quirks, and documentation while the team is also expected to ship domain features, off-the-shelf infrastructure starts to look less expensive.

The key is not whether the first loop is easy. It is whether the team is ready to own every failure mode behind that loop.

Further reading:

Read the whole story
alvinashcraft
10 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Neovim Clipboard on WSL: The One-Liner Fix

1 Share

Every time I set up Neovim on a fresh WSL instance, I hit the same wall: yanking text inside Neovim and pasting it into a Windows app (or vice versa) just doesn't work. "+y does nothing, and Neovim greets you with Clipboard: No provider, try :checkhealth. Nothing flows in or out of the clipboard, not even between files inside WSL.

The root cause is that WSL's Neovim can't talk to the Windows clipboard at all. The fix is a tiny Windows executable called win32yank that speaks the Windows clipboard API from the command line.

I've done this enough times now that I'm writing it down so I never have to search for it again. If you're here for the same reason, this one's for you.

Step-by-Step

1. Download win32yank

Grab the latest release from github.com/equalsraf/win32yank. Download win32yank-x64.zip and extract it to get win32yank.exe.

2. Place it in your WSL PATH

sudo mv /mnt/d/win32yank.exe /usr/local/bin/

Adjust the source path to wherever your browser downloaded it (usually /mnt/c/Users/<you>/Downloads/win32yank.exe).

3. Configure Neovim

Add this block to ~/.config/nvim/init.lua:

if vim.fn.has("wsl") == 1 then
  vim.g.clipboard = {
    name = 'win32yank-wsl',
    copy = {
      ['+'] = 'win32yank.exe -i --crlf',
      ['*'] = 'win32yank.exe -i --crlf',
    },
    paste = {
      ['+'] = 'win32yank.exe -o --lf',
      ['*'] = 'win32yank.exe -o --lf',
    },
    cache_enabled = 0,
  }
  vim.opt.clipboard = 'unnamedplus'
end

4. Done

Now y, "+y, "+p, right-click copy/paste — all of it flows through the Windows clipboard as you'd expect.



Bonus: One-Shot Setup Script

Next time I (or you) need this on a fresh box, run this single script. It downloads win32yank, installs it, and appends the config:

#!/usr/bin/env bash
set -euo pipefail

WIN32YANK_PATH="/usr/local/bin/win32yank.exe"
NVIM_CONFIG="${HOME}/.config/nvim/init.lua"
TMP_DIR=$(mktemp -d)

# Get the latest release tag from GitHub
echo "==> Fetching latest win32yank release..."
LATEST_TAG=$(curl -s https://api.github.com/repos/equalsraf/win32yank/releases/latest \
  | grep '"tag_name"' \
  | cut -d'"' -f4)

echo "==> Downloading win32yank ${LATEST_TAG}..."
curl -fsSL "https://github.com/equalsraf/win32yank/releases/download/${LATEST_TAG}/win32yank-x64.zip" \
  -o "${TMP_DIR}/win32yank-x64.zip"

echo "==> Extracting..."
unzip -q "${TMP_DIR}/win32yank-x64.zip" -d "${TMP_DIR}"
sudo cp "${TMP_DIR}/win32yank.exe" "$WIN32YANK_PATH"
sudo chmod +x "$WIN32YANK_PATH"
rm -rf "$TMP_DIR"

echo "==> Appending clipboard config to ${NVIM_CONFIG}..."
mkdir -p "$(dirname "$NVIM_CONFIG")"

cat >> "$NVIM_CONFIG" << 'LUA'

-- win32yank clipboard for WSL
if vim.fn.has("wsl") == 1 then
  vim.g.clipboard = {
    name = 'win32yank-wsl',
    copy = {
      ['+'] = 'win32yank.exe -i --crlf',
      ['*'] = 'win32yank.exe -i --crlf',
    },
    paste = {
      ['+'] = 'win32yank.exe -o --lf',
      ['*'] = 'win32yank.exe -o --lf',
    },
    cache_enabled = 0,
  }
  vim.opt.clipboard = 'unnamedplus'
end
LUA

echo "==> Done! Restart Neovim and yank away."

Save it as setup-wsl-clipboard.sh, run chmod +x setup-wsl-clipboard.sh and then ./setup-wsl-clipboard.sh.

Read the whole story
alvinashcraft
10 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Writing about it - approach to writer’s block

1 Share
Read the whole story
alvinashcraft
11 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Your Company Doesn't Need an AI Strategy

1 Share
From: AIDailyBrief
Duration: 24:49
Views: 200

...it needs an AI learning system. This episode argues that the Fable 5 disruption exposed a deeper enterprise problem: companies can’t treat AI as a vendor strategy. The real advantage will come from building learning systems that capture institutional judgment, workflow traces, private evals, and model-portable IP. In the headlines: could Anthropic and the White House be headed for a resolution?

Register for our new enterprise-grade AI training programs: ⁠http://training.besuper.ai/⁠

The AI Daily Brief helps you understand the most important news and discussions in AI.
Subscribe to the podcast version of The AI Daily Brief wherever you listen: https://pod.link/1680633614
Get it ad free at http://patreon.com/aidailybrief
Learn more about the show https://aidailybrief.ai/

Read the whole story
alvinashcraft
7 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories