devinside.Marcin Borecki Ideas. Systems. Evidence.
← The notebook

Engineering

Conversation State Is Not Application State: A .NET AI Reference Repository

A real ChatClientAgent read-tool roundtrip shows why a fresh conversation can forget history while SQLite preserves the current domain record.

Explore the mechanism ↓
Editorial ink and watercolor illustration for Forget the chat. Keep the domain record.

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

Conversation State Is Not Application State: A .NET AI Reference Repository

Editorial context: 28 May 2026 · Prepared and updated: 12 November 2026
Dates and technology context

This is a retrospective editorial timeline. Topic context: 28 May 2026. First publicly available: 12 November 2026. Current text, visuals and laboratory revisions were prepared on 12 November 2026.

Agents.AI1.28.0 (6Nov2026), MEAI11.0.0 (10Nov2026), SQLite10, current ChatClientAgent/session APIs and actual1,2,2 observations require12Nov2026 labels. Earlier-than-GA context must say preview.

Prepared on 12 November 2026. The reference uses Microsoft.Agents.AI 1.28.0, Microsoft.Extensions.AI 11.0.0 and a local SQLite database. Its chat provider is scripted; the framework and tool execution are real, but no hosted language model is called.

A conversation can remember a fact that the application has already changed. It can forget a fact that the application must preserve. Neither event is surprising once conversation history and domain state are treated as different kinds of storage.

The difficulty is that a fluent agent can make the distinction disappear. A previous message says that an item is approved, a summary repeats the statement, and a later tool call acts as though the conversation itself supplied authority. The system appears coherent because the language is coherent. Its underlying state may be wrong.

This article builds a small reference that makes the separation observable. A real ChatClientAgent calls a read-only application function. That function obtains the current record from SQLite under a trusted fixture scope. The conversation is allowed to contain an obsolete statement, but the domain result comes from the database. A fresh session can lose the earlier messages without losing the record.

The example is part of a shared laboratory with two companion articles. One examines restart and external effects. The other examines incoming events, object versions and overlapping work before execution. This article owns the conversational question: what may be forgotten, what must remain authoritative, and which boundary connects the two?

Three kinds of state need three different owners

01 / See the idea

Three states, three owners

Three states, three owners. A conversation can retain an obsolete statement while the database has a newer value. The tool reads the application record under trusted fixture scope rather than treating conversation text as authority.

A conversation can retain an obsolete statement while the database has a newer value. The tool reads the application record under trusted fixture scope rather than treating conversation text as authority.

Interpretation and evidence
  • Separate memory from authority.
  • Keep identity outside model arguments.
  • Reload facts whose lifecycle exceeds the chat.
Shared AI04 correctness.json and measurement-1..3.json; no production or hosted-model outcome.

Conversation history supports interaction. It records what was said, which tools were discussed and how a user arrived at the current request. It can make a response more understandable and reduce repeated explanation.

Request context carries information supplied by the application for the current operation. Identity, tenant scope, cancellation and the selected resource belong here when they are established outside the model. They should not become editable facts merely because they can be written into a message.

Domain state records the facts the application owns: the current object version, its value, whether an action is permitted, and which durable work has already been accepted. Those records have a lifecycle beyond a conversation.

In the reference, a domain object has an identifier, tenant, version, permission flag and value. The conversation can say the value is obsolete. The read function still returns the actual row. The trusted tenant is captured by application code rather than accepted as a model-generated argument.

This separation is not an instruction to place every conversational detail in a database. It is a way to decide which information can legitimately determine an effect. A user's wording may help identify what they want, while a domain record establishes the current state on which the application can act.

The same distinction applies to summaries. A summary can preserve context for the next interaction. It should not promote an uncertain statement into an authoritative record merely because it survived compression.

Use a real framework boundary with a deliberately simple provider

02 / See the idea

The function call is real; the model is scripted

The function call is real; the model is scripted. The actual Microsoft framework and function adapter execute the read. The script fixes tool choice, so this is integration evidence rather than a model-planning evaluation.

The actual Microsoft framework and function adapter execute the read. The script fixes tool choice, so this is integration evidence rather than a model-planning evaluation.

Interpretation and evidence
  • Name what is scripted and what executes.
  • Observe the domain function result.
  • Do not infer model quality from a deterministic call.
Shared AI04 correctness.json and measurement-1..3.json; no production or hosted-model outcome.

The reference constructs ChatClientAgent from the actual Microsoft Agent Framework package and supplies an AIFunction created from an application delegate. The framework is not represented by a hand-written imitation of its public methods. The resolved package graph is retained in a lockfile. Microsoft.Agents.AI 1.28.0.

The underlying IChatClient is scripted. On its first turn for a request, it emits a call to read_object. After a function result arrives, it emits a receipt confirming that it observed the tool result. It does not perform natural-language reasoning or decide which tool would be useful in an open-ended problem.

That limitation is intentional. It isolates the adapter and state boundary. If a test fails because the tool never executes, because the tenant scope is wrong or because a new session loses domain data, the failure cannot be blamed on model variability.

The reference does not claim that this scripted behavior establishes the quality of an agent's planning. A live model would introduce another evaluation problem: selecting the right capability, producing suitable arguments and communicating the result faithfully. The deterministic fixture gives that future work a clear integration boundary rather than pretending to replace it.

The distinction also keeps credentials and external services out of the experiment. No model account, paid endpoint or private customer data is required to reproduce the tool roundtrip.

Keep the function small enough to inspect

The read-only capability is deliberately narrow. It accepts an object identifier and returns the current authorized object. The request's trusted tenant is captured outside the function arguments:

string trustedTenant = "blue";
string ReadObject(string objectId)
{
    var obj = store.ReadAuthorized(objectId, trustedTenant);
    observed.Add(obj);
    return JsonSerializer.Serialize(obj);
}

The fixture tenant is not a production authentication system. It is a controlled input that makes the application boundary testable. The function does not accept a tenant identifier from the scripted model, so the model cannot widen its scope by changing an argument.

The store loads the row by identifier, checks tenant and permission, and returns the current version. An unauthorized read is refused. The test includes a foreign-tenant request to establish that the direct domain method enforces its declared scope.

The function is then registered through the actual library:

var tool = AIFunctionFactory.Create(
    (Func<string, string>)ReadObject, "read_object");
using var functions = new FunctionInvokingChatClient(provider)
{
    MaximumIterationsPerRequest = 4,
    AllowConcurrentInvocation = false
};

The bounded function-invocation loop is useful for this fixture because its expected behavior is exactly one tool call followed by a receipt. It is not evidence that four iterations is the right limit for another application. A limit should follow the task contract and the consequences of unfinished work.

Small functions also make failures easier to explain. The domain method owns authorization and current-state retrieval. The adapter owns exposing that capability to the agent. The provider owns the scripted call sequence. None needs to impersonate the responsibilities of the others.

Observe a changed record inside the same conversation

The first session asks the agent to read the current item while explicitly mentioning an obsolete remembered value. The scripted provider requests the read function. The function returns version one from SQLite.

The test then changes the domain row to version two with a new value. It does not rewrite the earlier conversation. A second request in the same session again asks for the current record, while acknowledging that the conversation still contains the old value.

The actual function invocation returns version two. That is the central correctness observation. The old conversational material remains part of the supplied interaction history, but it does not override the domain read.

This is not evidence that a real model would always disregard a contradictory sentence. The script always calls the tool. It demonstrates that the application can expose an authoritative path and that the framework roundtrip executes it under the stated scope.

A live model might still summarize the tool result incorrectly. A stronger application could render important structured fields directly, validate a proposed action against the current row, or require a version-qualified confirmation. Those choices belong to the next boundary, not to a claim that conversation history has become authoritative.

The practical rule is simple: if an important decision depends on current application state, provide a current-state capability and make the subsequent action check that state. Do not rely on the conversation to remain synchronized with every change elsewhere in the system.

Start a new session without resetting the domain

03 / Watch the mechanism

A fresh conversation keeps the current domain

A fresh conversation keeps the current domain. The executed three reads returned versions1,2and2. The animation explains a reset of interaction context without deleting the independently stored application record.
Enlarge diagram

Static view. Play to see how the mechanism unfolds.

The executed three reads returned versions1,2and2. The animation explains a reset of interaction context without deleting the independently stored application record.

Interpretation and evidence
  • Domain lifetime is not conversation lifetime.
  • A fresh session should not reconstruct facts by guessing.
  • Observed versions are fixture facts, not production history.
Shared AI04 correctness.json and measurement-1..3.json; no production or hosted-model outcome.

The test creates a second agent session and asks for the same item. The new session has no need to reproduce the old conversation in order to retrieve the current domain row. The function again observes version two.

The saved provider call counts make the session boundary visible. The first request supplies one message to the scripted provider, followed by a call containing the function interaction. The second request in the same session carries more history. A fresh session returns to the smaller initial message count.

The observed message counts were 1, 3, 5, 7, 1 and 3 across the six provider calls. These counts describe the exact nonstreaming fixture and configured adapter, not a universal guarantee about every Agent Framework session or provider.

The durable observation is that the three actual tool invocations saw domain versions 1, 2 and 2. Clearing the interaction context did not clear the application record. The database remained the source of the current value.

That is a useful form of memory separation. A user can begin a new conversation without asking the system to forget a business record. Conversely, an application can change a business record without requiring every old conversation to be rewritten before the change becomes real.

The design does not yet include a conversation-summary policy. A summary would need its own rules about which statements are retained, which are uncertain and which must be reloaded. The fixture shows why such a policy should be subordinate to authoritative state rather than a substitute for it.

A session identifier is not a business operation identifier

05 / See the idea

Four identities should not collapse into one

Four identities should not collapse into one. A conversation can discuss several jobs, while one job may outlive the conversation. Stable operation identity belongs to the application contract rather than the wording of a chat turn.

A conversation can discuss several jobs, while one job may outlive the conversation. Stable operation identity belongs to the application contract rather than the wording of a chat turn.

Interpretation and evidence
  • Keep durable effects independently identifiable.
  • Do not use a transcript as a transaction log.
  • Choose identifiers according to actual obligations.
Shared AI04 correctness.json and measurement-1..3.json; no production or hosted-model outcome.

The shared database also contains durable jobs and receiver effects. Those records are discussed in the companion articles, but their relationship to the conversation matters here.

A session can contain several requests. A single request may lead to several tool calls. A durable job can outlive the session that helped formulate it. Reusing one identifier for all of these concepts makes later questions difficult: which user task caused this effect, which attempt produced this observation, and which conversation merely discussed it?

The reference keeps object identifiers, event identifiers and workflow state separate from the framework session. The read-only agent test does not create a business effect, while the durable execution tests use explicit job identities without requiring the conversation to survive.

This is a design boundary, not a requirement that every small application needs an elaborate identity hierarchy. The identifiers should reflect obligations the system actually has. A disposable explanation may need no durable job. An action that must be reconciled after a lost reply needs a stable operation identity beyond a chat turn.

The important mistake to avoid is using the presence of a conversation transcript as evidence that an action happened or was authorized. A transcript can help reconstruct intent. The authoritative operation record establishes the state of the effect under the application's own contract.

That distinction becomes especially valuable when a model or framework changes. The conversation representation can evolve while the application continues to understand its durable obligations.

SQLite provides a local record, not a complete platform

The laboratory stores domain objects and jobs in one SQLite database and receiver effects in another. It uses real connections and transactions rather than an in-memory dictionary labelled persistent.

Microsoft.Data.Sqlite documents transactions as the boundary grouping statements into an atomic unit, with serializable isolation by default and one writer with pending changes at a time. Those properties inform the local implementation. They do not create a transaction spanning both database files and an imaginary remote business system. SQLite transactions in .NET.

For the read path, persistence is sufficient to demonstrate that domain state survives a new conversation. The example does not need a distributed store, a broker or cloud hosting to make that point. Adding those components would introduce additional contracts rather than strengthen this particular observation automatically.

The database is an owned temporary fixture. The runner creates a new directory with a declared prefix, refuses to reuse an existing directory and removes its own files after the checks. It does not open a production database or modify a user's existing data.

The package graph includes Microsoft.Data.Sqlite 10.0.0 and an explicitly resolved SQLite native bundle. The exact dependencies belong in the retained lockfile, because a statement about using SQLite is less reproducible than a record of the actual implementation loaded by the executable.

None of this proves performance or durability for a production workload. It establishes a concrete local storage boundary that another engineer can run and inspect.

The executed session evidence

04 / See the idea

The observed tool and provider counts

The observed tool and provider counts. The reference made six scripted provider calls and three actual application reads across two sessions. The counts demonstrate the examined roundtrip, not a model speed or intelligence score.

The reference made six scripted provider calls and three actual application reads across two sessions. The counts demonstrate the examined roundtrip, not a model speed or intelligence score.

Interpretation and evidence
  • Count the boundary actually exercised.
  • Keep call count separate from latency.
  • Preserve the session configuration with evidence.
Shared AI04 correctness.json and measurement-1..3.json; no production or hosted-model outcome.

The shared correctness run passed twenty-seven named checks across the three article scopes. This article relies primarily on the actual framework tool roundtrip, current-state observations and foreign-tenant refusal.

The session-specific observations are:

Observation Actual fixture result
Provider calls across three requests 6
Application read-function invocations 3
Domain versions returned 1, 2, 2
Provider message counts 1, 3, 5, 7, 1, 3
Domain record after fresh session Current version two remained available
Foreign-tenant domain read Refused

These are not answer-quality metrics. The provider's last message is a scripted receipt. The test asserts what the application function actually returned, not whether a language model reasoned correctly about it.

The code calls CreateSessionAsync and RunAsync on the installed framework, and function invocation occurs through the actual Microsoft abstractions. Successful compilation alone would not establish that path; the observed tool-call count and returned versions provide the execution evidence.

The test does not expose the read function through MCP. The editorial blueprint considered that transport, but this finite implementation stops at the real framework function boundary. A protocol adapter should be added only with its own identity, schema and transport checks. It would not be honest to describe the current run as an MCP roundtrip.

This limitation makes the article more useful, not less. A reader knows which components were actually exercised and where the next integration obligation begins.

Why a scripted conversation still reveals a real design problem

The strongest objection is that the scripted provider always makes the correct tool call. A real model may not do so, so what has the test learned?

It has established that the application-owned capability and framework wiring behave as intended when the request reaches the capability. It has also demonstrated that conversation reset does not require resetting domain state. Those are necessary properties of the proposed architecture, even though they are not sufficient for a dependable agent.

The next evaluation would ask whether a real provider selects the capability when needed and communicates its result accurately. It should include stale conversational assertions, ambiguous object references and attempts to supply identity through untrusted text. Its failures would need to be classified separately from wiring or storage failures.

Combining all these uncertainties in the first test would make a failure harder to diagnose. A clear deterministic boundary gives later model evaluation a stable target. The model can be useful without becoming the owner of authorization or the database's current value.

There is also a cost to overengineering this separation. A simple read-only assistant may not need a persistent workflow ledger. A small session may be handled adequately by ordinary message history. The additional components should follow actual obligations, not a belief that every agent requires a miniature enterprise platform.

The reference earns its storage by asking a specific question: can an interaction forget its history while the application preserves the state it is responsible for? The executed result answers that question within the fixture.

Build the next layer without changing the meaning of the first

A useful extension would add a deliberate history-reduction policy. It could remove old conversational material while preserving identifiers for records that must be reloaded. The acceptance condition would remain that current domain facts come from the authorized source, not from an increasingly confident summary.

Another extension could expose the same domain read through a transport such as MCP. That should preserve the capability's input and scope rules while testing the new boundary independently. A different route into the application should not change which identity is trusted.

An action-taking extension would require more. It would need a version-qualified proposal, an authorization decision and a record of the attempted effect. The read-only source result should not be treated as permission to act merely because it appears in the conversation.

The shared laboratory's restart and event articles address parts of those later obligations. They remain separate questions so that each result has a clear meaning. A session test is not a replay guarantee, and a replay test is not evidence that a model chooses the right action.

This approach favors a small number of well-understood boundaries over a large collection of impressive labels. The reader can see what changed at each layer and which earlier property should remain true.

Trace the read without retaining an entire life story

The fixture retains a small list of observed domain results and the provider's message counts. That is enough to establish its state-separation claim. It does not need to record a real person's conversations, preferences or work history.

This illustrates a useful design constraint for observability. Collect the evidence needed to explain the decision boundary, rather than assuming that maximum conversational retention produces maximum understanding. A tool call can be associated with an object identifier, domain version and outcome without turning every prompt into a permanent diagnostic record.

The exact retention choice depends on the application. Some investigations may require authorized access to the relevant text. Others can use controlled fixtures and identifiers. The important point is to make the purpose explicit and avoid treating the convenience of logging as a reason to collect unrelated material.

In this example, the tool's returned object and the known scripted messages are synthetic. The session evidence is therefore easy to preserve and publish. A production system would need a different policy for sensitive fields, access to diagnostic records and the lifetime of conversational data.

The state model helps make that policy more precise. Domain persistence, conversation retention and diagnostic retention need not share one schedule merely because all three can be serialized as JSON.

Do not turn a storage measurement into an agent-latency claim

The shared executable also contains a bounded measurement mode used by the integration owner. It prepares one completed operation and then performs one hundred duplicate admissions and one hundred completed-state reconciliation reads. Three fresh processes executed those batches.

The median times were 29.5469 milliseconds per hundred duplicate checks and 26.9909 milliseconds per hundred completed reconciliation reads. These are local SQLite operations with connection and transaction overhead inside the measured path. They are not timings for ChatClientAgent, a context reset, a model call or the read-only tool demonstration.

The storage measurements belong primarily to the restart and event discussions. Including their scope here prevents a tempting mistake: attaching the only available timing number to the most interesting component in the diagram. A shared executable does not make every measurement representative of every feature it contains.

There was no timed session-history comparison. The message counts show which messages the scripted provider received, while the domain versions show which records the function observed. Those are correctness observations, not a benchmark of memory compression or conversational efficiency.

If a later application needs a latency decision about long histories, it should measure that specific boundary with a declared provider, history size and interaction. It should also preserve the current-state contract while doing so. A faster conversation that acts on an obsolete fact would not be a successful replacement.

The separation between correctness evidence and performance evidence is especially important for agents because a fluent response can make every part of the system feel like one operation. The implementation should retain enough structure to say which work was actually observed.

Reproduce the observation before extending the design

The source package contains the project, local SDK pin, package lockfile and the complete fixture. The correctness command creates a new owned scratch directory, runs the session and durable-state checks, writes the evidence report and removes the temporary databases.

Reproduction should preserve the recorded package versions first. Changing the framework while reproducing a finding changes the experiment and may alter session or function-invocation behavior. That can be a useful follow-up, but it should have its own report rather than silently replacing the conditions of the original observation.

The code does not require a model key. Its scripted client is part of the fixture and remains clearly named. Replacing it with a live provider would require a separate decision about data, authority and evaluation. The successful local run establishes a starting boundary for that work, not advance approval of the larger integration.

Let the application remember what the conversation may forget

The core architectural choice is where the system places authority. Conversation history can guide the interaction, but important domain facts and durable obligations need a source whose lifecycle matches their consequences.

The reference demonstrates that separation with a real framework adapter and local persistent state. It does not require a hosted model to show the mechanism, and it does not pretend that a scripted provider proves agent intelligence.

Before adding another memory feature, ask what kind of information it is meant to preserve. Is it a conversational preference, a current domain fact, a request identity or an unresolved external effect? Who may change it, and what evidence would establish its current value?

Those questions determine the storage and API contract more usefully than the generic promise that an agent remembers. A dependable application can survive a new conversation because the conversation was never the only place its important state existed.

Source code and execution evidence

Download the complete lab, source record and raw execution results. This package supports the specific execution claims in this article. It is not a production certification.

THE AUTHOR

Marcin Borecki

Developer, entrepreneur and business angel. Writing about AI, software, data and the evidence behind technical decisions.

About the author →