Wiki · Research
Microsoft Agent Framework
Shelf
Research

Microsoft Agent Framework

1. Snapshot

FieldValue
Repositorymicrosoft/agent-framework
Pinned commit12621e0a746517068300f7b9445225c3ee2406ea
Date read2026-08-15
LicenceMIT — LICENSE
LanguagesC#/.NET and Python
Measured scale4,893 tracked files; 1,969 tracked .cs; 1,154 tracked .py; 945 C# files in dotnet/src; 623 Python files in python/packages

Measured 2026-08-15 at the pinned commit, in PowerShell:

(git ls-files | Measure-Object -Line).Lines
(git ls-files -- '*.cs' | Measure-Object -Line).Lines
(git ls-files -- '*.py' | Measure-Object -Line).Lines
(git ls-files -- 'dotnet/src/*.cs' 'dotnet/src/**/*.cs' |
  Sort-Object -Unique | Measure-Object -Line).Lines
(git ls-files -- 'python/packages/*.py' 'python/packages/**/*.py' |
  Sort-Object -Unique | Measure-Object -Line).Lines

The commands produced 4,893, 1,969, 1,154, 945, and 623. They count tracked paths, not logical lines of code or public APIs.

Read boundary. The .NET implementation is primary. I traced the GroupChatToolApproval sample from two ChatClientAgents through GroupChatWorkflowBuilder, the agent-host executor, external request/response handling, the in-process runner, checkpoints, workflow events, and OpenTelemetry. I then checked the Python core’s corresponding agent, tool, workflow, functional-workflow, session, approval, and observability surfaces for divergence. Hosted Azure services, provider transports, evaluation, declarative agents, realtime, and the separate durable extension are outside the boundary.

The repository describes the public surface as agents plus graph-based multi-agent workflows in both languages. Its .NET contributor map names AIAgent, AgentSession, ChatClientAgent, IChatClient, AITool, messages, and content as the core types (.NET source map).

2. The core loop

At the single-agent boundary, AIAgent.RunAsync normalizes overloads and delegates to RunCoreAsync. ChatClientAgent prepares a concrete session, messages, options, context providers, and history, calls the decorated IChatClient, records provider failure or new messages, updates the service conversation id, and returns AgentResponse (AIAgent run contract, ChatClientAgent.RunCoreAsync). The default client pipeline adds approval binding and bypass logic, function invocation, optional message injection and per-service-call history persistence, and an inert telemetry slot around the provider client (default decorator pipeline).

The traced multi-agent path starts in dotnet/samples/03-workflows/Agents/GroupChatToolApproval/Program.cs. It constructs QA and DevOps agents with different tools, wraps only the production-deployment function in ApprovalRequiredAIFunction, supplies a four-turn manager, and builds a group-chat workflow (agent and tool setup). Its custom manager selects QA for the first turn and DevOps thereafter (speaker selection).

GroupChatWorkflowBuilder turns each agent into an executor, creates one host with the canonical conversation, connects host-to-participant and participant-to-host edges, and designates host output plus participant intermediate output (builder expansion). The host’s manager selects the next participant from the full history; each participant keeps its own session while the host broadcasts only the new messages, and the manager’s iteration count bounds the loop (manager contract).

The runner processes work in supersteps. The sample starts a lockstep streaming run, sends a turn token, and consumes typed WorkflowEvents until completion or error. Agent outputs return through edges to the host, which either selects another speaker, reaches the manager’s limit, or halts for an external request (sample execution loop, StreamingRun contract).

3. State and persistence

There are three state layers in the traced .NET path. Each AIAgentHostExecutor holds an AgentSession and current-turn settings; the group-chat host holds canonical conversation and manager state; the in-process runner holds queued messages, external requests, edge state, and executor state (agent host state, manager checkpoint hooks).

A workflow checkpoint contains the superstep number, workflow description, runner data, executor state, edge state, and optional parent. CheckpointManager can use a process-memory implementation or JSON over an application-supplied ICheckpointStore; that store must return its index in commit order and namespaces records by session id (checkpoint payload, CheckpointManager, ICheckpointStore).

The human-in-the-loop checkpoint sample makes the recovery contract explicit: providing a manager creates a checkpoint after each superstep; a SuperStepCompletedEvent carries its identity; later RestoreCheckpointAsync rewinds the same run, including the pending external-input cycle (checkpoint and restore). Executor-local mutable state survives only if the executor implements checkpoint and restore hooks; the sample explicitly saves and reloads its attempt counter (executor state hooks).

Opinion. A checkpoint is recovery state, not a historical ledger. It represents one restorable execution point and may branch through parent ids; the application chooses the durable store, retention, tenant scoping, and authorization. The default manager is process memory, so its checkpoints do not survive a process crash (default manager).

4. Tools and the outside world

The framework relies on Microsoft.Extensions.AI tool primitives. A ChatClientAgent accepts AITools and injects FunctionInvokingChatClient when the caller’s client does not already contain one. Its own security contract says tools execute without approval by default, model arguments are untrusted, and side-effecting or irreversible tools should require explicit approval (constructor and security boundary, function layer insertion).

The deployment sample derives tool schema from C# methods and [Description] attributes. Three functions execute normally; only DeployToProduction is wrapped for approval (tool declaration and effects). Opinion. The callback still owns the side effect, validation beyond its declared input types, idempotency, compensation, credentials, and external authorization. Workflow checkpoints cannot make that outside-world effect transactional.

When an agent runs inside a workflow, AIAgentHostExecutor registers typed handlers for approval requests and function results. It invokes the agent with a per-agent session, collects unserviced requests, yields optional agent events, and submits pending requests to the workflow (handler registration, agent invocation and request collection). The runner stores the request by id and emits RequestInfoEvent; duplicate pending ids fail rather than overwrite (external request post).

5. Composition

The .NET surface offers both general graph construction and named multi-agent shapes. The general WorkflowBuilder connects typed executors with edges and explicit output designations. The agent facade builds sequential, concurrent, group-chat, handoff, and Magentic workflows (agent workflow factories).

The traced group chat is not peer negotiation. GroupChatManager.SelectNextAgentAsync owns speaker selection, receives the canonical history, and can filter the per-turn broadcast. The selected agent receives messages and an isolated session; its response crosses an edge back to the host, which retains orchestration ownership (manager selection and broadcast, host/participant graph). The manager’s base iteration count is checkpointed automatically; a subclass receives a prefixed state namespace for any additional cursor or model-session state (composition state boundary).

Output is a separate design choice from routing. The group-chat builder chooses the host as final output and participant agents as intermediate output; general builders can replace those designations. This prevents every internal executor payload from becoming caller-facing merely because it crossed an edge (default output designations).

6. The human in the loop

Approval is attached to a specific tool call, not to a plan or whole workflow. The sample displays the agent/port, tool name, and serialized arguments from RequestInfoEvent, then creates a typed approval response and sends it back on the same streaming run (approval event loop). StreamingRun queues that response for the next superstep; its event stream can either block while waiting or return control to a host that owns the turn loop (response and wait semantics).

The control is stronger than trusting the caller’s response payload. The default approval-binding decorator snapshots each model-originated pending request in AgentSession, keyed by request id. On resume it ignores unknown or duplicate approvals and rebinds an altered response to the exact tool name and arguments that were originally surfaced (approval binding contract, validation and rebinding, snapshot persistence).

Opinion. The framework supplies call identity, immutable request binding, pause/resume mechanics, and checkpointable workflow state. The host supplies the human or policy engine, authenticates the actor, authorizes the decision, records the reason if required, renders the UI, and stores state durably. The sample auto-approves in code; it demonstrates the protocol, not human authority.

What is recorded about a run?

Every non-streaming Run accumulates typed WorkflowEvents in a process-local list; consumers can read all outgoing events or only those since the last bookmark. StreamingRun exposes the same events in order as an async stream (Run event sink, WorkflowEvent base). Invocation, completion, failure, output, request, warning, and superstep types let application code and evaluation code read the run mechanically; the deployment sample itself handles request, streaming response, and failure events.

OpenTelemetry is a second channel. .WithOpenTelemetry is opt-in per .NET workflow and emits spans for build, workflow session/run, executor processing, edge routing, message delivery, and errors. The application owns the ActivitySource and exporter (telemetry activation, Aspire exporter sample). Raw inputs, outputs, and message content are excluded unless EnableSensitiveData is explicitly enabled (telemetry privacy default).

Opinion. The events are a rich run record and the spans are an operator-facing diagnostic record, but neither is an audit trail by itself. The event list is ephemeral, exporters and retention are application choices, content is intentionally absent by default, and the framework does not attach authenticated human identity to an approval. An audit trail needs a durable, access-controlled sink and an application-defined actor/policy record in addition to these signals.

7. The abstraction bargain

.NET/Python divergences at the pin

DifferenceClassificationEvidence and consequence
Python has a @workflow functional API that returns a stateless definition and requires build() for caller-scoped mutable execution; the inspected .NET workflow package uses builder/executor classes and has no FunctionalWorkflow or workflow attribute.Language idiom plus design surfaceFunctionalWorkflowDefinition, workflow decorator. The Python surface uses decorator syntax to make definition versus per-caller instance explicit.
.NET workflow telemetry is disabled until .WithOpenTelemetry; Python workflow entry points create spans subject to global observability settings, whose instrumentation default is enabled.Design surface.NET opt-in, Python workflow span, Python settings. Operators must know whether silence means disabled instrumentation or an unconfigured exporter.
.NET marks a local function by wrapping AIFunction in ApprovalRequiredAIFunction; Python’s @tool carries approval_mode="always_require".Language idiom.NET sample, Python decorator contract. Both reach request/response approval content; the declaration syntax differs.

Bounded absence check, 2026-08-15. rg -n 'FunctionalWorkflow|WorkflowAttribute|class .*WorkflowDefinition' dotnet/src/Microsoft.Agents.AI.Workflows returned no match. This establishes the inspected .NET package boundary, not the absence of every functional or declarative API in the repository.

One explicitly retired practice

The accepted provider-client decision records that Python core had bundled OpenAI and Azure provider implementations and dependencies. It retired that shape because it made core heavier than necessary and conflated core abstractions with one provider; the replacement extracts provider packages, keeps lazy compatibility imports, and quarantines deprecated wrappers for later deletion (problem and drivers, decision outcome). The same decision deprecates the assistants client and the V1 service client; the migration gallery then removes assistants-parity samples rather than teaching a deprecated surface (deprecated clients, removed migration samples).

Opinion. The enterprise bargain is explicit control surfaces rather than automatic guarantees. The framework makes sessions, approval, typed workflow events, checkpoint stores, output designations, telemetry, and provider separation available without inventing them in each application. A small project pays in concepts and configuration: decorator pipeline ordering, per-agent sessions, a manager or graph, event handling, state hooks, checkpoint serialization, exporters, and host-owned identity/retention policy. Omitting those pieces remains possible, but then “production-grade” describes available machinery rather than the application’s achieved properties.

Opinion. The approval binding is the strongest part of the bargain. It refuses a forged or mutated approval response at the framework boundary. Its limit is equally important: matching the approved bytes does not establish that the approver had authority or that the effect remains safe at execution time.

8. What rungs takes

These verdicts are inputs to WI-017; they do not change the catalogue here.

Pattern idVerdictPinned evidenceReason
session-handofftake-as-warningcheckpoint payload, event sinkOpinion. Recovery state, emitted history, and documentary intent are independent continuity layers; one cannot safely stand in for the others.
scope-disciplinetake-as-analogyprefixed manager stateOpinion. Shared state needs an ownership namespace just as work needs an ownership boundary; a convenient global bag otherwise turns composition into collisions.
candidate: approval-bound-to-requesttakebinding contract, rebindingOpinion. Approval is valid only for the exact surfaced action identity and arguments, and it should be consumed once; a later caller-supplied payload must not redefine what was approved.
candidate: resumable-approval-statetakeworkflow request post, checkpointed HITL sampleOpinion. A pending request belongs in recoverable state with stable identity and a typed response path; the host can then own timing, UI, and authority.
candidate: event-stream-not-audit-logtake-as-warningWorkflowEvent, telemetry privacy defaultOpinion. Events and spans are audit inputs, not accountability: durability, retention, access control, actor identity, and decision reasons must be named separately.
candidate: explicit-output-designationtakegroup-chat output mappingOpinion. Internal progress should become public output only through an allow-list; graph connectivity alone must not decide disclosure.

The strongest counter-evidence is that most enterprise properties remain opt-in or host-owned: checkpoints default to memory, .NET workflow telemetry defaults off, sensitive content defaults off, approval defaults off for tools, and the framework does not authenticate an approver or retain an audit ledger.