Migrating Agent Behavior, Not Just SDK Calls
A migration can compile successfully, pass a demonstration and still change the behavior that made an agent useful. The new implementation may choose different tools, forget state at a different boundary or retry an operation that the old implementation treated as terminal.
Explore the mechanism ↓
Reading tools Listen or translate
Read aloud
Uses an installed voice on your device.
Translate article
Machine translation. Code and evidence notes stay in English.
Original English text.
START WITH THE MECHANISM
Interactive companion and laboratory update: 12 November 2026. The editorial date places the topic on the timeline; this current explanation and its measured evidence retain their actual preparation and execution dates.
THE FULL INVESTIGATION
Migrating Agent Behavior, Not Just SDK Calls
Dates and technology context
This is a retrospective editorial timeline. Topic context: 14 January 2026. First publicly available: 12 November 2026. Current text, visuals and laboratory revisions were prepared on 12 November 2026.
Keep January2026 as an Agent Framework public-preview discussion. The actual adapter comparison uses later stable Microsoft.Agents.AI1.28.0 and MEAI11.0.0 on .NET10.0.15, executed 12 November2026.
A migration can compile successfully, pass a demonstration and still change the behavior that made an agent useful. The new implementation may choose different tools, forget state at a different boundary or retry an operation that the old implementation treated as terminal.
This is especially easy to miss when the migration is described as replacing SDK calls. A method name changes. A conversation object becomes a session object. A tool registration moves to another builder. The diff looks mechanical, while the operational contract changes underneath it.
The better unit of migration is behavior. What inputs can the agent accept? What actions may it request? Which state must survive? What counts as a completed task? Which failures must stop execution? Those questions belong to the application and should remain answerable even when the orchestration library changes.
The technical frame here is .NET 10 and an evaluation of Microsoft Agent Framework during its public-preview period. Microsoft announced that preview on 1 October 2025. This article does not retrospectively describe the preview as a stable release or import later APIs into it. Microsoft Agent Framework preview announcement.
Inventory the contract you actually have
Start by recording the existing application's observable behavior. Include the prompts, tool definitions, model configuration, authorization rules, state storage, cancellation behavior and operational limits. These are not all equally important, but leaving them implicit makes comparison impossible.
Distinguish desired behavior from accidental behavior. The old implementation may have a bug that users learned to work around. It may also have an undocumented safeguard that nobody noticed because it prevented incidents quietly. A migration should not preserve every accident, but it should identify which differences are intentional.
Create a decision log for those differences. “The new SDK behaves differently” is an explanation of mechanism, not a product decision. If a change affects user expectations, permission boundaries or recovery, somebody needs to accept that change explicitly.
The inventory should describe outcomes rather than implementation details. “An unauthorized customer identifier never reaches the data store” is a contract. “We call this middleware method” is one way the current implementation attempts to uphold it. The first belongs in the migration test; the second belongs in the adapter notes.
Make a model-independent event ledger
01 / See the idea
Compare through an application ledger
For a hypothetical denied customer request, both adapters must expose the policy decision and whether a downstream call occurred. Comparing only the final wording would miss an unsafe effect.
Interpretation and evidence
- Freeze scenario and initial state.
- Record business events, not only callbacks.
- Compare outcomes independently of prose.
Conversation transcripts are useful for understanding language, but they are a poor sole representation of execution. Tool calls, authorization decisions and committed effects deserve structured records that do not depend on how a particular SDK formats a chat message.
The following application-owned .NET 10 types are illustrative and unexecuted. They are not Microsoft Agent Framework API declarations.
public sealed record ScenarioInput(
string ScenarioId,
string UserRequest,
string ActorFixture,
string InitialStateFixture);
public sealed record ObservedEvent(
long Sequence,
string Kind,
string Name,
string Outcome,
string? ResourceId);
public sealed record ScenarioResult(
string ScenarioId,
string ImplementationId,
IReadOnlyList<ObservedEvent> Events,
string FinalStateHash,
string TerminalStatus);
The ledger should record decisions at the application boundary, not only callbacks emitted by the framework. Otherwise the two implementations may produce different internal events that cannot be compared meaningfully, or both may omit the same important business effect.
Avoid storing secrets or complete prompts by default. A scenario identifier can refer to an authorized test fixture. The ledger should contain the evidence needed for comparison, while sensitive content remains governed by its own retention and access rules.
Build adapters around the same business services
Keep the old orchestration and the candidate orchestration behind a small interface. Both should call the same deterministic domain services, with the same authorization and idempotency controls. This reduces the number of moving parts in the comparison.
// Test-harness boundary, not an SDK-specific adapter.
public interface IAgentScenarioRunner
{
string ImplementationId { get; }
Task<ScenarioResult> RunAsync(
ScenarioInput scenario,
CancellationToken cancellationToken);
}
Semantic Kernel plugins already encourage encapsulating application capabilities behind functions and injected services. That separation is useful during migration because the domain capability can remain stable while the orchestration adapter changes. Semantic Kernel plugin concepts.
Do not run both implementations against the same writable customer systems as an initial comparison. Use isolated fixtures and recording destinations. Shadow execution is only safe if the shadow cannot issue real effects, or if a carefully designed downstream mechanism prevents duplication. A comment saying “shadow mode” is not that mechanism.
For a concrete preview baseline, record Microsoft.Agents.AI version 1.0.0-preview.251001.1 rather than a floating prerelease reference. The package registry explicitly identifies it as a prerelease. The harness examples remain application-owned code, so they do not pretend that later SDK syntax belongs to this preview. A runnable adapter must also lock its connector dependencies and be tested independently. Microsoft Agent Framework preview package.
Compare invariants before comparing prose
Two correct answers can use different words. Two identical answers can hide different and potentially unsafe execution paths. Start with invariants that do not require a language model to judge them.
Did the agent access only permitted resources? Did a write occur only after the required authorization? Did the run terminate within its application-defined action bound? Did cancellation stop new work? Did the final durable state satisfy the expected business condition?
Some invariants concern ordering. A validation must happen before an effect. Others concern cardinality: a payment intent must not be committed twice. Others concern absence: a denied operation must produce no downstream call. Represent these distinctions directly rather than flattening every result into a single “pass” score.
Only after the execution contract is checked should you compare answer usefulness. A migration may improve the prose while making the system less predictable. If the product depends on a particular output structure, validate that structure separately from the quality of the explanation.
State migration is the part that deserves suspicion
02 / A concrete example
Same answer, different responsibility
The old path checks policy before calling a destination; the candidate calls first and refuses later. Identical final text cannot make those execution paths equivalent.
Interpretation and evidence
- Ordering is an invariant.
- Denied means no effect.
- The answer is not the execution ledger.
An agent's state can include conversation history, tool results, summaries, pending actions and durable workflow progress. These pieces have different ownership and retention requirements. Treating them as one serialized conversation object makes a migration fragile.
Define an application-level state schema. Record its version and distinguish durable facts from derived summaries. A model-generated summary is useful context, but it should not become the only record that a customer approved an action or that a tool committed a change.
Consider what happens to a conversation already in progress. Can it finish using the old implementation? Can the candidate safely read its state? Does switching implementations require a conversion, and can that conversion be reversed? Starting a new conversation successfully does not answer any of these questions.
A conservative rollout can route existing sessions to their original implementation while creating new sessions on the candidate. This reduces conversion risk but increases the period during which both systems must be operated. An eager conversion can simplify operations sooner, but it demands stronger evidence about state compatibility. Neither choice is automatically superior.
Match scenarios without pretending to control randomness
Use the same scenario inputs, tool fixtures and initial state for both implementations. Record the model deployment and configuration as well. If the provider changes behavior during the experiment, that change can affect the comparison independently of the SDK.
A matching random seed, when supported, should not be treated as a universal guarantee of identical behavior across implementations or hosted-model revisions. Compare repeated runs and distributions. Look for stable differences in tool selection, completion rate, refusals, retries and latency.
Keep difficult scenarios visible. A candidate that improves common questions while regressing a rare authorization case should not pass because the average score rose. Group results by failure consequence and question family. Hard safety invariants should have their own acceptance gate.
The migration fixture should include ambiguity. An agent may need to ask a clarifying question rather than choose a convenient interpretation. A new implementation that eagerly calls a tool can look more efficient while actually moving uncertainty into an irreversible operation.
Test the uncomfortable interruptions
03 / Watch the mechanism
Timeout is not proof of failure
Static view. Play to see how the mechanism unfolds.
In this hypothetical migration case, the destination accepted the first action but its response disappeared. A candidate that retries immediately changes the recovery contract.
Interpretation and evidence
- Persist an operation identity.
- Represent an unknown outcome.
- Reconcile before issuing another effect.
Successful runs are the easiest part of migration. The interesting differences often appear when a dependency times out, a tool returns malformed data or the process disappears between an effect and its acknowledgement.
Test interruption before a tool request, during the request and after the tool reports success. Test a stream that stops halfway through an answer. Test cancellation while a retry is scheduled. Test a denied operation whose error message includes content that could be mistaken for a new instruction.
The application should preserve the distinction between failed and unknown outcomes. A timeout while waiting for a write response does not prove that the write failed. If the candidate automatically retries where the old implementation reconciled, that is a substantive behavior change.
Also test resource closure. A cancelled run should not leave unobserved tasks, open streams or timers that continue issuing work. A migration that passes functional scenarios while gradually leaking resources is not operationally equivalent. The test needs to inspect the aftermath, not merely the returned answer.
Keep observability comparable
Frameworks expose different internal events. Preserve a common application layer of operation identifiers, tool outcomes and terminal states so that the dashboards used during rollout remain interpretable.
Record implementation version as a bounded diagnostic dimension. Keep individual run identifiers in traces or structured audit records rather than exploding metric dimensions. Use consistent duration boundaries: if the old measurement begins before retrieval and the new one begins after it, the migration will appear to improve latency by moving the stopwatch.
OpenTelemetry can carry observations across implementations, but sharing a transport does not make the semantics identical. The application still needs to define what its measurements mean. Microsoft's .NET instrumentation guidance separates producing activities from collecting them, which is a useful boundary for adapter-independent evidence. Distributed tracing instrumentation in .NET.
During comparison, retain enough detail to explain a disagreement. A red dashboard tile is not an explanation. The operator should be able to identify the scenario, the differing event sequence and the durable state produced by each implementation.
Plan rollback as a state problem
A deployment rollback is simple only if the state remains readable and the effects remain compatible. If the candidate changes the session schema or produces new external actions, reverting the binary may not restore the previous operating model.
Define rollback conditions before rollout. Which regressions trigger an immediate stop? Which can be tolerated while gathering evidence? Who owns sessions that are already executing? What happens to proposals created by the candidate but not yet committed?
Prefer additive state changes and explicit schema versions where possible. Keep irreversible migrations out of the same deployment step as the first behavioral trial. If a conversion cannot be reversed, treat it as a separate release decision with its own recovery plan.
A canary should be small enough to contain risk but representative enough to exercise the important boundaries. Sending only trivial requests to the candidate produces a reassuring metric and weak evidence. Choose a declared subset of operations, monitor the same contract and preserve the ability to route new work back.
The migration report should describe changed responsibility
An honest report says which behaviors were preserved, which changed intentionally and which remain uncertain. It separates static code review, executed deterministic tests, model evaluations and production observations. These are different levels of evidence.
No matched execution has been performed for this manuscript. The proposed harness, fixtures and acceptance gates are a design for obtaining that evidence. A future result should identify exact package versions, model configuration, fixture hashes, execution dates and failed as well as successful scenarios.
The important question at the end is not whether the new SDK feels more elegant. It is whether the application still knows what it is responsible for, and whether the team can prove that responsibility survives ordinary failures. Once that is established, new capabilities become a reason to migrate. Before it is established, a compiling sample is only a beginning.
Execution appendix, 12 November 2026: an actual adapter comparison
The earlier article uses a January 2026 preview-era frame. This appendix is a later experiment, executed on 12 November 2026. It does not claim that the packages used here were available in that earlier frame, and it does not retroactively turn the earlier illustrative code into executed code.
The new laboratory compares three real callers of one domain service: an ASP.NET Core endpoint, Microsoft.Extensions.AI automatic function invocation and Microsoft Agent Framework's ChatClientAgent. The local provider implements IChatClient and emits predetermined function calls. It is explicitly a test double, not a live model or a measurement of reasoning quality.
The run uses SDK 10.0.400 and runtime 10.0.15. The restored packages include Microsoft.Extensions.AI 11.0.0 and Microsoft.Agents.AI 1.28.0. Their public registrations place them in November 2026, and the actual code verifies that this selected combination executes on the pinned runtime. Microsoft.Extensions.AI 11.0.0, Microsoft.Agents.AI 1.28.0.
This is not a claim that an arbitrary Semantic Kernel application can be mechanically migrated. The experiment isolates a narrower question: when adapter plumbing changes, can the same command preserve its authorization, durable state and effect count? That is a useful prerequisite for a larger migration because a fluent answer cannot compensate for a changed effect contract.
Freeze the boundary that both implementations must preserve
The command consists of operation, quote and approval identifiers. Its actor and tenant are supplied separately as immutable request context. The domain service checks the tenant, asks the reservation store to authorize and reserve the operation, then dispatches through a local receiver whose effect has a stable identity.
The input fixtures deliberately hold the business command constant across callers. A valid request should produce one receiver effect and a Succeeded state. An unauthorized request should not produce a new effect. An exact retry should return the existing result. An unknown result should remain reconcilable without another charge or duplicated effect.
The test creates a new scenario store for each caller. This avoids an easy migration-testing mistake: running adapter A first, leaving its operation record behind, then observing adapter B successfully replay that record and calling the outcomes equivalent. Here each adapter must establish the result from the same initial state.
The expected outcome comes from the scenario definition. The HTTP path is also used as the reference for comparing the ordered domain trace, but it is not the sole correctness oracle. Every path must independently satisfy the effect-count and state expectation. Three adapters that all repeat the same error would not be excused merely because their traces match.
This distinction is especially important for denied operations. A new adapter might return a different error format while preserving the required absence of an effect. That can be an acceptable interface change if clients are updated deliberately. Returning the same text while creating an effect would be a substantive regression.
Separate allowed adapter changes from forbidden domain changes
01 / Watch the mechanism
Adapter changes stop at the domain boundary
Static view. Play to see how the mechanism unfolds.
Transport and orchestration are allowed to differ, while the command context, domain trace and receiver outcome are compared under equivalent initial state.
Interpretation and evidence
- Use a fresh fixture per adapter.
- Allow deliberate presentation differences.
- Reject changed business effects.
The experiment allows each entry path to have its own transport behavior. HTTP returns a status code and a serialized result. The chat paths receive a function-call request, invoke the application function and send a function result back to the scripted provider. Each chat path takes two provider rounds in these scenarios.
Those differences are recorded rather than normalized away invisibly. They are allowed because the test's acceptance contract concerns domain state, receiver effects and the ordered application trace. It does not require the adapters to have identical internal events or text formatting.
The Agent Framework path uses the actual constructor and run method:
var agent = new ChatClientAgent(
functions,
instructions: "Execute the supplied fixture command once.",
name: "FixtureAgent",
tools: [tool]);
await agent.RunAsync("Run the controlled command.");
The functions client is already configured for serial function invocation with a finite iteration limit. The tool closes over the request-local context and delegates to the same domain method as HTTP. The framework supplies another execution layer; it does not become the authority for actor identity or approval scope. ChatClientAgent constructors.
This is a practical migration seam. If a later framework version changes orchestration metadata, it should not require rewriting the policy service. If it changes how function arguments are bound, the adapter integration tests should fail before that difference reaches a real user action.
The local provider's deterministic behavior removes one source of uncertainty from this comparison. It does not eliminate the need for a later live-model evaluation. A model could choose different tools, omit required arguments or respond differently to a denial even while the deterministic service remains correct.
Compare traces that describe responsibility
02 / See the idea
Nine scenarios agree across three adapters
The table reads the actual check report. Approval reuse is denied after one earlier authorized effect, so D / 1 means no additional effect rather than no historical effect.
Interpretation and evidence
- Compare exact outcomes and ordered traces.
- Keep positive and negative controls.
- Do not treat one shared run as three independent studies.
The shared trace records application events such as receiving the command, accepting tenant scope, returning a reservation state, returning a dispatch state and reconciling an unknown outcome. It does not depend on the names of SDK callbacks or private implementation classes.
For each of nine scenarios, the test compares the final outcome and the ordered domain trace across all three paths. The accepted run completed 75 checks over 27 combinations. The scenario set covers valid execution, wrong actor, wrong tenant, expiry, stale resource version, changed quote, exact replay, consumed-approval reuse and an unknown outcome resolved after reopening the store.
The trace is useful because it can locate a difference. If an adapter bypasses tenant checking, the event sequence changes. If a retry becomes a fresh operation, the receiver count or reservation behavior changes. If a framework catches an application exception and invents a success result, the independently inspected state remains capable of exposing the discrepancy.
The test's outcome record is intentionally small:
public sealed record Outcome(
string State,
long Effects,
long Held,
long Committed);
The simplicity keeps the comparison about consequences. A production migration may need more fields, including schema versions, tool arguments, external references and categorized failures. Those fields should be added because they represent meaningful differences, not because one SDK emits a larger event payload than another.
The current trace remains in memory and is exported in the test result. It is not a crash-safe audit journal. Durable operation state belongs to the SQLite store, while the trace helps explain the execution that the harness observed.
Exercise the migration where ordinary demonstrations stop
A valid action is necessary evidence, but it is the easiest case for a new adapter to satisfy. The more revealing scenarios change a condition around the operation's authority.
The wrong-tenant fixture uses an actor name that is valid elsewhere but belongs to a different tenant context. Identity-looking fields are included in tool arguments and HTTP JSON, yet those values cannot replace the host's selected context. The denial therefore tests the direction of trust rather than just a malformed identifier.
The changed-quote fixture attempts to use approval for a different immutable quote. Expiry and resource-version scenarios invalidate conditions that once held. These cases ask whether the adapter preserves a refusal that depends on current domain state, rather than merely forwarding a happy-path command.
Replay scenarios ask a different question. Repeating the exact operation should retain one effect. Reusing the approval for another operation should be denied while leaving the first effect intact. An outcome saying Denied with one existing effect can therefore be correct; the test must distinguish no new effect from no effect ever having occurred.
The unknown-outcome scenario is especially relevant to migrations because retry behavior can change across libraries. The receiver commits, the acknowledgment is withheld, and the ledger retains an Unknown state. After reopening the store, reconciliation establishes success without dispatching the effect again. The adapter comparison confirms that this behavior is preserved across entry paths.
None of these scenarios simulates every timing race. The store's transactional and idempotency rules come from the accepted local reservation design. The new contribution is that actual adapter plumbing reaches and preserves those rules, including the negative paths.
Measure adapter work without pretending it is model latency
The local timing fixture compares successful idempotent replays through the domain directly, through function middleware and through Agent Framework. The operation has already succeeded before timing starts, so the test measures recovery of existing state rather than a new effect or first-time database commit.
Each sample performs sixteen replay calls. Six measured samples follow one warm-up sample for each route. The chat routes include creation of their local wrappers, function binding and the two scripted provider rounds. The direct route omits those adapter layers. HTTP is not part of the timing comparison, although its real request path is part of the correctness suite.
The counter records process-wide managed allocation because the API paths are asynchronous. This can include allocation from other activity in the same process, so it should not be read as an exact object inventory for one method. The serialized measurement lane reduces competing task activity without creating a fully isolated laboratory machine.
The shared measurement completed on 12 November 2026 at 14:05 UTC on .NET 10.0.15, with exit code zero. It contains six samples for each path. The table reports milliseconds per successful replay and process-wide managed bytes allocated per replay; all three paths are retained.
| Local replay path | Median ms | Observed ms range | Allocated bytes range |
|---|---|---|---|
| direct-domain | 1.298 | 1.267–1.374 | 37,203.5–37,222.5 |
| meai | 1.338 | 1.318–1.406 | 42,435.5–42,498.5 |
| maf | 1.435 | 1.351–1.583 | 49,907.5–49,978.5 |
The medians differ, but the observed timing ranges overlap. This small single-process run does not establish a statistically reliable overhead percentage or identify which internal layer caused a difference. Connection creation, SQLite reads, validation, wrapper construction and scripted function calls are all part of the declared boundaries. No empty-loop or storage baseline was subtracted.
The allocation counter covers the process because the calls use asynchronous APIs. It does not measure retained heap, native SQLite allocations or a live provider's memory. Each replay is checked for the expected Succeeded state and exactly one receiver effect. That correctness gate is more important than a lower time obtained by accidentally omitting policy or persistence work.
These are deliberately local measurements. The direct route does not include HTTP, and neither chat route includes a remote model request. Their labels identify the exercised code paths, not the latency a user should expect from an AI application. The same raw report is shared across the three article appendices and is counted as one experiment, not three independent repetitions.
Use the result as one migration gate, not the whole verdict
The recorded comparison supports a bounded statement: this selected set of adapters preserved the tested domain outcomes and traces for these fixtures. It does not establish that every framework feature is interchangeable, that sessions migrate automatically or that the same live model will make the same decisions.
A complete migration would add the boundaries used by the actual application. Persisted session state needs conversion tests. Streaming needs interruption and partial-output behavior. Tool selection needs representative evaluations. Existing clients need compatibility checks for allowed response-format changes. The current fixture supplies a reusable domain gate underneath those concerns.
Keep the old and candidate adapters pointed at controlled destinations until the behavioral evidence is sufficient. A shadow run that can still issue real external actions is not safely passive merely because its output is ignored. This local test avoids that problem by using a synthetic receiver and independent scenario stores.
The useful migration report then separates what changed intentionally, what remained equivalent and what was not examined. This appendix records one such equivalent boundary. That is a stronger result than saying the new sample compiles, while remaining much narrower than claiming the migration is universally complete.
Historical versions and verification boundaries
These pins existed before the context date. Availability and static API checks do not establish a successful build or a production-safe patch level. Execution claims are limited to explicitly identified companion checks.
| Component | Version | Available by | Source |
|---|---|---|---|
| .NET SDK | 10.0.100 | 2025-11-11 | Primary record |
| .NET runtime | 10.0.0 | 2025-11-11 | Primary record |
| C# | 12.0 | 2024-12-03 | Primary record |
| Microsoft.SemanticKernel | 1.0.1 | 2023-12-18 | Primary record |
| Microsoft.Agents.AI | 1.0.0-preview.251001.1 | 2025-10-01 | Primary record |
| OpenTelemetry | 1.9.0 | 2024-06-14 | Primary record |