Inference-driven development with Copilot; pros and cons

Inference Driven

Source of Truth: docs/ARCHITECTURE.md
Status: Canonical repo cleanup aligned to the current VS MCP Bridge and BlogAI narrative as of 2026-05-16. Bracket-style tokens are intentional BlogEngine/GwnWikiExtension tokens.

Inference-Driven Software Design With Copilot: Pros and Cons

Using AI Assistance Without Turning Development Into A Black Box

Copilot is a useful development assistant. It can complete patterns, suggest code, write tests, and keep a developer moving through mechanical work. The risk is not that Copilot is useless. The risk is treating inference as if it were architecture, verification, and judgment all at once.

That distinction is the heart of the BlogAI story. AI-assisted software design works best when generated code is surrounded by source-of-truth documents, observable workflows, approval boundaries, logs, diagrams, and durable evidence. Without those things, a team can move faster while understanding less.

VS MCP Bridge has become a practical case study for that lesson.

What Inference Means In Practice

In software development, inference means the model is producing likely code, explanations, or next steps from the context it can see. That can be powerful, but it is not the same as owning the system model.

The model may know common patterns. It may mirror nearby code. It may produce a convincing implementation. But it does not automatically know which boundaries are non-negotiable, which logs are required for future triage, which security claims would overstate the current system, or which documentation is the source of truth.

That is why inference-driven development needs a workflow around it.

Where Copilot Helps

Copilot works well when the local task is clear and the surrounding code already teaches the pattern.

  • It can accelerate repetitive edits, tests, and small refactors.
  • It can suggest idiomatic code when the project conventions are visible.
  • It can help explore unfamiliar APIs or fill in routine structure.
  • It can reduce friction when the developer already knows what should happen.

In that role, Copilot behaves like a fast assistant. It is especially useful when the developer can review the output against a clear contract.

Where Inference Becomes Risky

The same strengths become risky when the task is architectural, security-sensitive, or poorly bounded.

  • A generated change may look correct while bypassing the real execution boundary.
  • A suggested log line may leak data or pollute MCP stdout.
  • A local refactor may erase a correlation id that future troubleshooting depends on.
  • A plausible explanation may imply authentication, sandboxing, or secret storage that does not exist.
  • A quick fix may solve the symptom while leaving no evidence for the next session.

These are not reasons to avoid AI tools. They are reasons to stop treating prompt-to-code as the whole workflow.

Prompt-To-Code Is Not Enough

The early mistake in many AI-assisted workflows is assuming that the prompt ends when code appears. In practice, the better workflow is prompt-to-evidence.

A useful AI-generated change should be answerable:

  • What boundary did it touch?
  • Which source-of-truth document says this behavior is correct?
  • Which tests or validation steps prove it?
  • Which logs or artifacts would explain it later?
  • Which Mermaid diagram reflects the observed flow?
  • What should a future AI session read before extending it?

That is the difference between code generation and engineering discipline.

How VS MCP Bridge Changed The Workflow

The VS MCP Bridge cleanup made this concrete. The project did not become clearer just because an AI generated code. It became clearer because logs, diagrams, handoffs, and architecture documents exposed where the system was vague.

Sequence diagrams helped reveal transport boundaries. Trace logs made request and operation correlation visible. Durable artifacts showed whether execution really flowed through the expected catalog and executor path. Approval and security traces forced a clearer distinction between current plumbing and future hardening.

That evidence led to better architecture:

  • the MCP stdio boundary stayed clean
  • the VSIX stayed isolated behind the named-pipe boundary
  • compiled tools gained descriptors, requests, results, catalogs, and executor-owned logging
  • MEF became a discovery seam instead of an execution shortcut
  • approval-aware execution became part of the tool boundary
  • security seams stayed explicit without claiming production authentication or sandboxing
  • audit and redaction became part of reconstructable tool execution

In other words, the AI assistance was useful because the project kept forcing it back through observable architecture.

Human Review Still Owns The Design

Copilot can propose. Codex can implement. ChatGPT can explain tradeoffs. None of those tools should silently own the design.

Human review still decides whether a change matches the architecture, whether the risk is acceptable, whether the evidence is enough, and whether the documentation tells the truth. The stronger the tool, the more important that review becomes.

This is especially true for security and approval workflows. A model can generate a policy class or approval hook, but the project still needs to say what is intentionally deferred: OAuth, user identity, real secret storage, sandboxing, signed plugin manifests, tamper-evident audit stores, and SIEM export are not complete just because a seam exists.

Source Of Truth Beats Chat Memory

One of the strongest lessons from this project is that durable source files beat chat memory.

The current workflow asks future sessions to start from files such as:

Those files make the system teachable. They also reduce the chance that an AI session resumes from an outdated mental model.

Where BlogAI Fits

BlogAI can help turn this architecture work into learning material, but only if the blog stays aligned with the code.

That is why the current blog cleanup starts from preserved database exports, canonical repo sources, manifest metadata, and explicit token/link rules. Blog posts should not drift away from the system they are explaining. They should point readers back to the current architecture, trace workflows, Mermaid sources, and handoffs that support the claims.

Done well, BlogAI becomes more than a publishing surface. It becomes a way to keep project knowledge synchronized with code, validation artifacts, and operational lessons.

Related Mermaid Trace Sources

The following diagram sources directly support the story:

Those .mmd files remain the diagram source of truth. The post references them directly instead of embedding generated images.

Practical Pros And Cons

Practice Strength Risk
Copilot as coding assistant Fast local implementation help Can produce plausible but wrong code if review is weak
Codex-style implementation sessions Can inspect, edit, validate, and commit cohesive slices Needs repository source-of-truth and validation constraints to stay grounded
Architecture chat and review Good for explaining tradeoffs and surfacing assumptions Can become speculative if not tied back to code and artifacts
Durable traces and handoffs Make AI-assisted work reconstructable Require discipline to keep current

Takeaway

Inference-driven development is useful when it is not treated as autonomous development.

The stronger pattern is human-directed, evidence-backed AI assistance: use Copilot for local acceleration, use Codex or chat tools for broader implementation and reasoning, require source-of-truth documentation, preserve trace evidence, and keep approvals, logs, and boundaries visible.

That is what VS MCP Bridge is trying to teach. The goal is not just prompt-to-code. The goal is prompt-to-evidence, with code as one result of a workflow that remains understandable after the session ends.

See Chat Sessions Models And Agents for related background on chat sessions, models, and agents.

WPF VSIX Threading: Understanding UI Switching, Async Behavior, and Pipe Safety

Source of Truth: docs/ARCHITECTURE.md
Status: Canonical repo cleanup aligned to the current VS MCP Bridge and BlogAI narrative as of 2026-05-16.

WPF VSIX Threading: Understanding UI Switching, Async Behavior, and Pipe Safety

Why Reliable AI Tooling Depends On Reliable Host Boundaries

AI-assisted workflows only feel trustworthy when the host runtime is trustworthy. In a Visual Studio extension, that means WPF state, Visual Studio APIs, async work, and pipe-backed requests must respect the UI thread instead of treating it as an implementation detail.

VS MCP Bridge is a useful example because it has several boundaries active at the same time: MCP stdio, a local named pipe, Visual Studio APIs, a WPF tool window, proposal approval state, and shared tool execution. If those boundaries blur, the AI layer may look unreliable even when the real problem is host-thread misuse.

The Core Rule

The Visual Studio UI thread is a scarce resource. Treat it that way.

  • Do transport, parsing, validation, and file-independent computation off the UI thread.
  • Switch to the UI thread only for WPF state, Visual Studio shell access, editor access, or UI-bound services.
  • Do the smallest possible amount of work after switching.
  • Return to async background execution naturally after the UI-sensitive work is complete.

The goal is not to eliminate switching. The goal is to make every switch intentional, narrow, and easy to explain in logs or traces.

Why UI Locks Happen

Most VSIX threading problems come from a few familiar patterns:

  • blocking on async work with .Result or .Wait()
  • doing expensive work after switching to the UI thread
  • switching too early and carrying too much execution on the UI thread
  • letting pipe or transport code manipulate WPF state directly
  • calling Visual Studio APIs from background code without isolating the UI-thread requirement
  • assuming an await preserves thread affinity for the rest of the method

Those problems are not cosmetic. They can make tool calls hang, approval UI state appear stale, or diagnostics point at the wrong layer.

Every Await Is A Boundary

A common source of confusion is code shaped like this:

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(ct);
// UI work

var data = await _service.GetDataAsync(ct);

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(ct);
_viewModel.Apply(data);

The second switch is not redundant. The first switch makes the immediate continuation UI-thread-safe. The later await introduces another suspension point. After that awaited operation completes, code that touches WPF or Visual Studio state should re-establish the UI-thread requirement.

If code after an await must touch UI or Visual Studio state, switch intentionally at that point.

Pipe Safety Starts With Separation

The named pipe is not the UI. It is a local transport boundary.

In VS MCP Bridge, pipe code should handle message reading, serialization, dispatch, validation, cancellation, and transport diagnostics. It should not update WPF controls, mutate viewmodel state directly, or treat Visual Studio APIs as if they were background-safe.

The safe shape is:

MCP request
  -> stdio-safe MCP server
  -> local named-pipe client
  -> pipe server dispatch
  -> host service
  -> minimal UI-thread switch only where host state requires it
  -> structured response

That separation matters because MCP stdout must stay clean. Diagnostics belong in stderr, file logs, UI logs, trace artifacts, and structured failures, not stray stdout lines that corrupt protocol traffic.

Visual Studio Access Belongs Behind The Host Boundary

Visual Studio APIs are host-specific and often UI-thread-sensitive. The MCP server should not own that knowledge. Shared tool code should not own it either.

The VSIX host is the correct place to isolate Visual Studio access:

public async Task<string> GetActiveDocumentPathAsync(CancellationToken ct)
{
    await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(ct);
    ThreadHelper.ThrowIfNotOnUIThread();

    return _vsAdapter.GetActiveDocumentPath();
}

Everything outside that narrow section can remain async and background-friendly. That keeps host correctness visible and stops UI-thread requirements from leaking through the whole codebase.

Transport, UI Orchestration, And Execution Are Different Boundaries

One of the architecture lessons from VS MCP Bridge is that not all boundaries are the same.

  • Transport boundary: MCP stdio and the local named pipe move requests and responses.
  • Host boundary: the VSIX owns Visual Studio services, DTE access, editor state, and UI-thread switching.
  • UI orchestration boundary: the presenter and viewmodel own visible tool-window state and proposal review surfaces.
  • Execution boundary: BridgeToolExecutor owns shared tool policy, approval, redaction, audit, correlation, and structured results.

Threading bugs often happen when these responsibilities collapse into one another. A pipe handler should not become a UI controller. A presenter should not become a transport layer. A discovered tool should not bypass the executor. A model suggestion should not silently decide any of that.

Proposal State Makes Threading Visible

The proposal workflow is where threading, UI state, and AI-assisted tooling meet.

An MCP client can submit a proposed edit. The request crosses the named-pipe boundary. The VSIX host creates proposal state and displays it in the tool window. The user approves or rejects it. Apply happens only after approval, and terminal outcome state is shown back in the UI.

That workflow depends on host correctness. If UI state is updated from the wrong thread, or if async callbacks are reused after a proposal completes, the user sees confusing behavior. It may look like the AI tool is unreliable, but the real defect is usually lifecycle or thread ownership.

The current architecture separates proposal lifecycle ownership through IProposalManager, presenter orchestration, and viewmodel state. That makes the workflow easier to reason about and test.

Diagnostics Expose Hidden Execution Order

The project improved when logs and Mermaid traces made execution order visible.

For host correctness, the important question is not only "did this call succeed?" It is also:

  • Which request id was active?
  • Which layer received the request?
  • Did the request cross the pipe boundary?
  • Did the VS service operation start?
  • Did the code switch to the UI thread only where required?
  • Did visible UI state update after the host work completed?
  • Did terminal proposal state clear correctly?

When those answers are visible, troubleshooting becomes a boundary-localization exercise instead of a guessing game.

Correct Pattern: Background First, UI Last

A safe workflow keeps background work and UI work separate:

public async Task<ResponseDto> HandleRequestAsync(RequestDto request, CancellationToken ct)
{
    var parsed = Parse(request);
    var result = await _worker.ProcessAsync(parsed, ct);
    return result;
}

Then the UI layer applies the result intentionally:

public async Task RefreshAsync(CancellationToken ct)
{
    var result = await _service.HandleRequestAsync(_request, ct);

    await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(ct);
    _viewModel.Apply(result);
}

That pattern keeps transport logic, host work, and UI presentation from becoming a tangled blocking path.

Related Mermaid Trace Sources

The following diagram sources make these boundary rules concrete:

Those .mmd files are the diagram source of truth. This post references them directly rather than embedding generated images.

Practical Checklist

  • Assume background execution by default.
  • Switch to the UI thread as late as possible.
  • Keep UI-thread sections small and explicit.
  • Never block on async work.
  • Keep pipe and transport code UI-agnostic.
  • Keep MCP stdout clean; send diagnostics through approved channels.
  • Keep proposal lifecycle state owned by the proposal/presenter/viewmodel boundary.
  • Log request ids, operation names, success or failure, and elapsed timing at meaningful boundaries.
  • Use durable traces when a workflow matters enough that a future session must reconstruct it.

Takeaway

Reliable AI tooling depends on reliable host/runtime boundaries.

In a WPF VSIX, that means switching to the UI thread only when the host actually requires it, keeping pipes and stdio transport-safe, separating UI orchestration from execution, and making important workflows observable through logs and diagrams.

Switch late, do little, leave quickly, and leave evidence.

That pattern keeps the extension responsive and makes AI-assisted workflows easier to trust, diagnose, and evolve.

Understanding Dependency Injection (DI)

IOC

LinqPad Script: WeatherForecastR5.linq (12.09 kb)

I'll start at the end (literally) and give the key information you'll need to know about dependency injection.  WebApi and ASP.NET Core applications use a dependency injection system to instantiate classes; in the case of this application, when a route is selected (figure 10b lines 211-213) the class for that route is instantiated and then invoked, e.g., HomePage, WeatherPage, and ToggleService.

the IOC system (which I'll just refer to as system) will look in its service collection registrations (figure 10a lines 174-183) to not only instantiate the class, but also provide its parameters.  The registrations will tell the system how to instantiate a class, e.g., as Transient (new instance each request), Scoped (per session / request), and Singleton (everyone shares the same instance).  The difference between scoped and singleton is that if 5 people hit the Website at the same time, each will get their own scoped instance, which is isolated from the other 4 users.  Within a session, the scoped instance behaves as a singleton, but only for that user.   Where singletons instances will be shared by "every" user.

The system uses constructor injection to instantiate and invoke the class [and its parameters].   By default, the system will look for the constructor with the largest number of parameters, get instances for each of the parameters, instantiate the class, and then invoke the class constructor with the parameters.   All classes and parameters must be declared in the service registrations, aka "container".    

Note that as each parameter is instantiated, that it's constructor parameters are also looked up in the container, instantiated and provided.   This is referred to as propagating the dependency chain; as long as "new" is never used to instantiate a class (breaking the chain) then you'll be able to simply put an interface or class in any class constructor and the system will give you an instance for it. 

Understanding this is the key, and paramount, to understanding the IOC/DI system.  It is the essence of Inversion of Control (IOC), aka Dependency Injection (DI).  Inversion of control meaning that instead of you instantiating a class, providing all of the constructor parameters, and invoking the class - the system does it for you.


Figure 1. Overview of application running

With basics out of the way.  All that remains is understanding the function of each class.  We'll cover each of the following with an overview of each classes code.  You'll find that there is a clear separation of concerns with each having a single responsibility; there is not a lot of code in each class, it does one thing, and it does it well.


Figure 2.  Skeleton view of application components

The following are the HomePage, WeatherPage, and ToggleService.  For the home page we'll introduce a second IOC Unity Container, unlike the system's container, the Unity Container supports Setter injection (discussed below) and allows you to register additional interfaces, classes, and factories on the fly.   With the system container, you'll find that you can only register during system bootstrapping - once the container is built, you cannot add any more registrations.  

You'll see that we provide an instance of IUnityContainer [in image below] and use it to instantiate (resolve) the IWeatherFormatter instance.   This uses a factory pattern, that based on the current value of IsJson (figure 10a lines 166-171) the container will provide either a JsonFormatter or TableFormatter instance.

Setter injection will kick in because these implementations of IWeatherFormatter both have the property below;
   [Dependency] Public IFoo Bar {get;set;} 

The [Dependency] tells the Unity container that it needs to populate this property in the same manner as it does constructor parameters; it provides an instance.  This is referred to as Setter injection you'll find that the system and unity both use different values (reference figure 10b and the comments on line 198-203 as to why).

Armed with the knowledge of setter injection, you should now be able to look at the code in figure 9 for Foo and understand how the "Bar" class will return "This is FooBar" for it's GetMessage() function.  

Figure 3.  Pages and service

Below we see the results of the HomePage being clicked with the TableFormatter.


Figure 4. Home page

Below we show the results of the WeatherPage being clicked with TableFormatter


Figure 5. Weather forecast page

Below we show that the ToggleService will toggle the IsJson property which is then returned (via bodyHtml) to the invoking process (in HtmlBase figure 11).  Once the state is toggle any subsequent Home or Weather clicks will result in json being displayed.


Figure 6. Toggle service

Below is the key parts to the HtmlBase, which our HomePage, WeatherPage, and ToggleService derive from.


Figure 7. HtmlBase class

Below we show our TableFormatter and JsonFormatter components


Figure 8. Formatters (json and html table)

We use IFoo to demonstrate how dependencies are propagated, and automagically populated, by either constructor or setter injection.


Figure 9. Foo

The magic happens in the container.  The system will require that all dependencies are registered so that it knows how to instantiate a components lifetime (transient, scoped, or singleton) and provide an instance.  Below the code is commented.


Figure 10a First part of WebAppBuilderExtension

Here we show how we can do a late registration (after build on line 204) and as a result change the setting for IFoo in the unity container - it will have a different implementation now then the system.   We also demonstrate how MiddleWare can use these registrations - it will send information to the console base on the registered implementation of its constructor parameters.


Figure 10b Second part of WebAppBuilderExtension

GetHtml() below is how our pages display their content with javascript code handling button clicks and clock updates.


Figure 11.  GetHtml() code 

The decoupled nature of IOC / DI will allow for easy reuse of components as it is ultimately the container that can pick and chose its implementation for any of its interfaces.


Figure 12 - where the MiddleWare parameters are displayed

How to publish your own blog [SmarterAsp]

This blog is available on GitHub: BlogEngine.NET (Billkrat fork) 

Once you have the source code available you can publish it to a SmarterASP.NET host for as little as $2.95 a month (see add on bottom right); having your own blog doesn't have to be expensive nor hard to deploy/setup.

  1. Figure 1 Creating a new site in SmarterASP
  2. Figure 2 Show Deployment Information
  3. Figure 3 Get the Web Deploy publish information

    In Visual Studio
  4. Figure 4 Add a new profile and select "Import Profile"
  5. Figure 5 Point to the file you downloaded from SmarterASP
  6. Figure 6 Publish your site


Figure 1 Creating a new site in SmarterASP 


Figure 2 Show Deployment Information


Figure 3 Get the Web Deploy publish information


Figure 4 Add a new profile and select "Import Profile"


Figure 5 Point to the file you downloaded from SmarterASP


Figure 6 Publish your site