Chapter 3

Agents and instructions

Create a Python MAF agent with a clear role, instructions, and model client boundary.

Objective

After this chapter, you will be able to create a Python Microsoft Agent Framework agent by choosing a chat client, giving the agent a clear name and instructions, and invoking it with await agent.run(...).

This chapter targets agent-framework==1.9.0.

Concept/Theory

In practice, an agent is not just a prompt and not just a model call. It is a behavior boundary: a model receives user input, follows durable instructions, and works through an application-controlled client boundary. Chapter 1 introduced this as the model plus behavior constraints, available tools, and runtime support for task-oriented interaction.

Instructions are the part of that boundary that says what role the agent should play, what style it should use, and what constraints should shape its answers. Microsoft Foundry describes instructions as one of an agent's core components alongside the model and tools (Microsoft Learn). They guide the model, but they do not replace application controls such as tool scoping, validation, middleware, or tests.

The chat client is the model/provider boundary. Keeping it separate from the agent lets you express behavior once while choosing the provider configuration that runs inference. MAF's agent documentation describes agents as components that coordinate user interaction, model inference, and tool execution in a structured runtime loop (Microsoft Learn).

In MAF

For normal provider-backed chat agents, use agent_framework.Agent. It wraps a chat client, durable instructions, optional tools, middleware, context providers, and run/session state. The inspected signature for agent-framework==1.9.0 is:

Agent(
    client: SupportsChatGetResponse[OptionsCoT],
    instructions: str | None = None,
    *,
    id: str | None = None,
    name: str | None = None,
    description: str | None = None,
    tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
    default_options: OptionsCoT | None = None,
    context_providers: Sequence[ContextProvider] | None = None,
    middleware: Sequence[MiddlewareTypes] | None = None,
    require_per_service_call_history_persistence: bool = False,
    compaction_strategy: CompactionStrategy | None = None,
    tokenizer: TokenizerProtocol | None = None,
    additional_properties: MutableMapping[str, Any] | None = None,
) -> None

The OpenAI provider client used in the worked example is agent_framework.openai.OpenAIChatClient. Its inspected constructor is:

OpenAIChatClient(
    model: str | None = None,
    *,
    api_key: str | Callable[[], str | Awaitable[str]] | None = None,
    credential: AzureCredentialTypes | AzureTokenProvider | None = None,
    org_id: str | None = None,
    base_url: str | None = None,
    azure_endpoint: str | None = None,
    api_version: str | None = None,
    default_headers: Mapping[str, str] | None = None,
    async_client: AsyncOpenAI | None = None,
    instruction_role: str | None = None,
    compaction_strategy: CompactionStrategy | None = None,
    tokenizer: TokenizerProtocol | None = None,
    middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
    function_invocation_configuration: FunctionInvocationConfiguration | None = None,
    additional_properties: dict[str, Any] | None = None,
    env_file_path: str | None = None,
    env_file_encoding: str | None = None,
) -> None
ParameterUse it for
clientThe provider chat client that performs model inference. Any compatible chat client can sit behind the agent boundary.
instructionsDurable role and behavior guidance: tone, constraints, and what the agent should optimize for.
nameA readable identity for logs, traces, multi-agent displays, and debugging.
toolsOptional callable capabilities the agent may request when instructions alone are not enough.
model / api_keyOpenAI client configuration. Live calls require credentials such as OPENAI_API_KEY.

Invoke the agent with await agent.run(messages). In 1.9.0, there is no public run_stream method; streaming is selected with agent.run(..., stream=True), which returns a response stream. If you construct message objects directly, use agent_framework.Message and text content created with Content.from_text(...).

Agent.run(
    self,
    messages: AgentRunInputs | None = None,
    *,
    stream: bool = False,
    session: AgentSession | None = None,
    middleware: Sequence[MiddlewareTypes] | None = None,
    tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
    options: OptionsCoT | ChatOptions[Any] | None = None,
    compaction_strategy: CompactionStrategy | None = None,
    tokenizer: TokenizerProtocol | None = None,
    function_invocation_kwargs: Mapping[str, Any] | None = None,
    client_kwargs: Mapping[str, Any] | None = None,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]

When to use / pitfalls

Lean on instructions when the behavior is about role, tone, decision criteria, or response shape. Add tools when the agent needs to retrieve facts, call application logic, or perform side effects. The Microsoft Agent Framework overview recommends using agents for open-ended or conversational tasks and autonomous tool use, while using ordinary functions or workflows when the path is deterministic (Microsoft Learn).

  • Use instructions for behavior: "Answer in one concise paragraph" belongs in instructions.
  • Use tools for capability: "Look up the order status" should be a scoped function or hosted tool, not a vague instruction to guess.
  • Use a workflow for known process order: if every request follows the same steps, make the order explicit instead of asking the model to plan it.

Common pitfalls

  • Vague instructions: instructions like "be helpful" do not define role, boundaries, or output expectations.
  • Missing client configuration: constructing OpenAIChatClient without a usable model and credential will fail when you make a live call.
  • Expecting instructions to enforce safety: instructions guide behavior, but application code still needs validation, tool limits, tests, and observability.
  • Inventing streaming APIs: use agent.run(..., stream=True); do not call a nonexistent run_stream.

Worked example

The example below is adapted from backend/examples/ch03/agent_run_structure.py. It constructs the agent and session without making a network call unless OPENAI_API_KEY is set.

Build an instruction-bound MAF agent and guard the live OpenAI call.
from __future__ import annotations

import asyncio
import os

from agent_framework import Agent, AgentResponse, AgentSession, Content, Message
from agent_framework.openai import OpenAIChatClient


def build_agent() -> Agent:
    client = OpenAIChatClient(
        model="gpt-4o-mini",
        api_key=os.getenv("OPENAI_API_KEY") or "placeholder",
    )
    return Agent(
        client,
        instructions="Answer in one concise paragraph.",
        name="concise-assistant",
    )


async def main() -> None:
    agent = build_agent()
    session = AgentSession(session_id="example-session")

    local_response = AgentResponse(
        messages=[Message("assistant", [Content.from_text("Local response shape.")])]
    )
    print(local_response.text)

    if not os.getenv("OPENAI_API_KEY"):
        print(f"Constructed {agent.name!r} with session {session.session_id!r}; skipping live call.")
        return

    # requires OPENAI_API_KEY
    response = await agent.run("Explain what an Agent is in one sentence.", session=session)
    print(response.text)


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

Step by step: OpenAIChatClient defines the provider boundary; Agent(..., instructions=..., name=...) defines the behavior boundary; AgentSession gives the run a reusable session identity; and await agent.run(...) sends the live request only after credentials are present. The local AgentResponse line is there to show the response/message shape without requiring a model call.

Recap & next

  • An MAF agent is the application boundary around a chat client, instructions, optional tools, middleware, and run/session state.
  • instructions should define role and constraints; they do not replace programmatic controls.
  • OpenAIChatClient supplies the model/provider boundary used by the standard Agent.
  • Invoke agents with await agent.run(...); use stream=True for streaming.
  • Credential-requiring examples should be guarded and marked clearly.

Next, Chapter 4: Chat clients, messages, and conversations goes deeper on provider clients, message construction, and multi-turn state.