Chapter 4

Chat clients, messages, and conversations

Understand how MAF separates agent behavior from provider chat clients, messages, and conversation state.

Objective

After this chapter, you will be able to explain how Microsoft Agent Framework (MAF) separates agent behavior from the provider chat client, build provider-neutral Message objects, and choose how to carry multi-turn conversation state.

Target library version: agent-framework==1.9.0.

Concept/Theory

The chat client is the model boundary

In Chapter 3, the Agent owned behavior: name, instructions, tools, and the run boundary. The chat client is the dependency behind that agent. It is the provider-facing object that knows how to send normalized messages to an inference service and return normalized responses.

This decoupling is why MAF can present agents as a common abstraction while still supporting different model providers. Microsoft Learn describes MAF agents as using chat client implementations from inference services, and its provider guidance notes that provider capabilities vary across tools, structured outputs, file search, MCP tools, and background responses (agents, providers).

Messages, roles, and conversation turns

The vocabulary from Chapter 1 still applies: an agent perceives input, reasons with a model, and responds or acts. At the chat-client layer, that loop is represented as a sequence of messages. Each message has a role such as system, user, assistant, or tool, plus one or more content items.

Conversation state is not magic memory. For a direct chat-client call, you usually pass the prior turns again as Message objects, and some providers can also use a provider conversation identifier through options. At the agent level, MAF adds higher-level session concepts for longer-running conversations; the direct client remains the lower-level model boundary.

In MAF

For Python agent-framework==1.9.0, use the public chat-client and message symbols below. These are the verified names for this chapter; do not substitute provider-native message shapes or an unverified TextContent class.

Type What it represents Verified API shape
agent_framework.BaseChatClient Abstract base for provider-independent chat clients. get_response(messages, *, stream=False, options=None, compaction_strategy=None, tokenizer=None, function_invocation_kwargs=None, client_kwargs=None)
agent_framework.SupportsChatGetResponse Protocol-like target when your code needs any object with chat response behavior. Use for abstractions and tests instead of depending on one concrete provider.
agent_framework.openai.OpenAIChatClient Concrete OpenAI Responses API-style chat client with middleware, telemetry, and function invocation support. OpenAIChatClient(model=None, *, api_key=None, credential=None, org_id=None, base_url=None, azure_endpoint=None, api_version=None, ...)
agent_framework.foundry.FoundryChatClient Concrete Azure AI Foundry chat client for a Foundry project endpoint and model deployment. FoundryChatClient(*, project_endpoint=None, project_client=None, model=None, credential=None, allow_preview=None, ...)
agent_framework.Message A normalized conversation turn. Message(role: RoleLiteral | str, contents: Sequence[Content | str | Mapping] | None = None, ...)
agent_framework.Content A unified content container for text, URI, function call, or function result content. Content.from_text(text), Content.from_uri(uri, ...), Content.from_function_call(...), Content.from_function_result(...)
RoleLiteral The standard message roles. Literal["system", "user", "assistant", "tool"]
ChatResponse / ChatResponseUpdate Normalized final response and streaming update shapes. ChatResponse(messages=..., response_id=..., conversation_id=..., model=..., ...); ChatResponseUpdate(contents=..., role=..., conversation_id=..., ...)
ChatOptions Provider-independent request options. TypedDict keys include model, temperature, max_tokens, tools, response_format, conversation_id, and instructions.

A direct call returns either an awaitable ChatResponse or, when stream=True, a stream of ChatResponseUpdate values ending in a final ChatResponse. Use ChatOptions for common settings, but remember that Microsoft documents provider capability differences; not every provider honors every option the same way (provider guidance).

When to use / pitfalls

Use the chat client directly when...

  • You need a simple model call with explicit message history and no agent behavior.
  • You are writing a provider adapter, test seam, or small utility that should target BaseChatClient or SupportsChatGetResponse.
  • You want to inspect normalized Message, ChatResponse, or streaming ChatResponseUpdate shapes before adding tools or orchestration.

Use an Agent when...

  • The application needs durable behavior: instructions, identity, tools, middleware, sessions, or future workflow composition.
  • You want the framework runtime to coordinate the agent loop described in Microsoft Learn's agent documentation (agents).

Pitfalls

  • Provider configuration is still real configuration. The example below skips the live call unless OPENAI_API_KEY is set.
  • Do not mix OpenAI and Azure routing casually. OpenAIChatClient accepts OpenAI-style options such as api_key, base_url, and Azure-related options such as azure_endpoint and api_version; configure one intended route clearly.
  • Do not invent content classes. In the inspected API, use Content.from_text(...); there is no public top-level TextContent.
  • Conversation IDs are provider hints, not universal memory. Keep explicit message history when correctness depends on prior turns, and use agent sessions when you need higher-level conversation management.

Worked example

The example builds two provider-neutral messages, creates local response data without credentials, and then performs a guarded live OpenAIChatClient.get_response call only when OPENAI_API_KEY is available.

Example: messages, Content.from_text, ChatOptions, and a guarded OpenAI chat-client call. Credential required for the live call: OPENAI_API_KEY.
"""Chapter 4: Messages, content, options, and guarded chat-client call."""

from __future__ import annotations

import asyncio
import os

from agent_framework import ChatOptions, ChatResponse, Content, Message
from agent_framework.openai import OpenAIChatClient


def build_messages() -> list[Message]:
    return [
        Message("system", ["You are concise."]),
        Message("user", [Content.from_text("Say hello in five words or fewer.")]),
    ]


async def main() -> None:
    messages = build_messages()
    options: ChatOptions = {"temperature": 0.2, "conversation_id": "example-conversation"}
    local_response = ChatResponse(messages=[Message("assistant", ["Hello from local shape."])])
    print(messages[1].text)
    print(local_response.text)

    if not os.getenv("OPENAI_API_KEY"):
        print("Skipping live OpenAIChatClient.get_response call; set OPENAI_API_KEY to run it.")
        return

    # requires OPENAI_API_KEY
    client = OpenAIChatClient(model="gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"])
    response = await client.get_response(messages, options=options)
    print(response.text)


if __name__ == "__main__":
    asyncio.run(main())

Walkthrough: build_messages() creates a system turn and a user turn. The user content uses Content.from_text, although plain strings are also accepted in Message contents. ChatOptions sets a low temperature and a sample conversation_id. The local ChatResponse demonstrates the normalized response shape before the optional provider call runs.

Recap & next

  • The chat client is the provider/model boundary; the agent is the behavior boundary.
  • Use Message, Content.from_text, and role literals to keep conversation turns provider-neutral.
  • Use ChatResponse, ChatResponseUpdate, and ChatOptions for normalized outputs, streaming updates, and request settings.
  • Carry multi-turn state deliberately: explicit prior messages, provider conversation IDs where supported, or higher-level agent sessions.

Next, Chapter 5: Tools and function calling shows how an agent moves beyond text by calling controlled application capabilities.