Tensor LabsTENSORLABS

From Prompt to Workflow: Multi-Step Python Agents with Microsoft & Copilot

At Microsoft Build 2026 in early June, the Microsoft Agent Framework reached general availability and the GitHub Copilot SDK hit 1.0 support inside it for both

July 3, 20267 min read12 sectionsBy Ahmed Abdullah
From Prompt to Workflow: Multi-Step Python Agents with Microsoft & Copilot

Introduction

At Microsoft Build 2026 in early June, the Microsoft Agent Framework reached general availability and the GitHub Copilot SDK hit 1.0 support inside it for both .NET and Python. You will use the Python path to build an agent that takes a multi-step task, calls tools to get real data, reasons across the results, and returns an answer, with the framework handling the run loop and the observability you would otherwise hand-roll. The example builds a release-notes drafter that reads merged pull requests and writes a changelog.

What the Microsoft Agent Framework gives you

The Agent Framework is the run loop you stop writing yourself. You define an agent with a model, a set of tools, and instructions. You call run, hand it a task, and the framework manages the back-and-forth: it asks the model what to do, executes the tool the model picks, feeds the result back, and repeats until the model produces a final answer. It also threads observability through every step, so you get traces of which tool fired with which arguments without instrumenting anything.

This is the part people underestimate. The hard part of an agent was never the first model call. It was the loop, the tool dispatch, the error handling when a tool throws, and the visibility into what the agent actually did. The framework owns all four.

What the GitHub Copilot SDK adds

The Copilot SDK is how the agent gets its model. Instead of wiring up a raw provider key, you authenticate through a GitHub Copilot subscription and the SDK gives the Agent Framework a chat client backed by Copilot's models. For a team already paying for Copilot, the model access is something you already have, governed by controls you already set.

Install and configure the client

Install the framework and the Copilot SDK, then build the chat client the agent will use.

code
pip install agent-framework github-copilot-sdk
python
# client.py
import os
from github_copilot_sdk import CopilotChatClient
chat_client = CopilotChatClient(
token=os.environ["GITHUB_COPILOT_TOKEN"],
model="gpt-5.5-copilot", # the model Copilot serves your org
)

The token comes from your Copilot subscription. The model name is whatever Copilot exposes to your organization; you do not manage a separate provider account.

Define a tool the agent can call

Tools in the Agent Framework are plain Python functions with type hints and a docstring. The framework reads the signature and the docstring to build the schema the model sees, so the docstring is not decoration, it is the interface.

python
# tools.py
import subprocess
import json
def list_merged_prs(since: str) -> str:
"""List pull requests merged since a date (YYYY-MM-DD).
Returns JSON, one object per PR, with 'number', 'title', 'author'.
"""
out = subprocess.run(
["gh", "pr", "list", "--state", "merged",
"--search", f"merged:>={since}",
"--json", "number,title,author"],
capture_output=True, text=True, timeout=30,
)
return out.stdout or "[]"

The docstring states the argument format and the return shape. The model relies on it to call the tool correctly, so write it the way you would write a function contract, not the way you would write a comment.

Create the agent with its tools

Now assemble the agent: the chat client, the instructions, and the list of tools it may use.

code
# build_agent.py
from agent_framework import ChatAgent
from client import chat_client
from tools import list_merged_prs
agent = ChatAgent(
chat_client=chat_client,
instructions=(
"You draft release notes. Use the tools to gather merged PRs, "
"then write a concise changelog grouped by Features, Fixes, and "
"Chores. Use only PRs the tools return. Invent nothing."
),
tools=[list_merged_prs],
)

The instruction "invent nothing" is doing real work. An agent with a tool that returns data will still happily pad the answer with plausible entries that were never in the data unless you tell it the tool output is the only allowed source.

Run a multi step task

Calling run hands the task to the framework's loop. The agent decides to call list_merged_prs, reads the result, and writes the changelog, all inside one call from your side.

python
# run_once.py
import asyncio
from build_agent import agent
async def main():
result = await agent.run(
"Draft release notes for everything merged since 2026-06-15."
)
print(result.text)
asyncio.run(main())

You wrote no loop, no tool dispatch, no message threading. The framework did the round trips. From your code it looks like one async call that happens to be smart.

Add a step guard and read the trace

A framework that runs the loop for you can also run it too long. Cap the steps, and use the run's built-in trace to see what actually happened.

python
# run_guarded.py
import asyncio
from agent_framework import RunOptions
from build_agent import agent
async def main():
result = await agent.run(
"Draft release notes for everything merged since 2026-06-15.",
options=RunOptions(max_iterations=8),
)
for step in result.steps: # the framework's trace
if step.tool_call:
print(f"called {step.tool_call.name}({step.tool_call.arguments})")
print("---")
print(result.text)
asyncio.run(main())

The max_iterations cap is the same discipline every agent needs: a hard stop so a confused model cannot loop forever. The result.steps trace is what you get for free here that you would have built by hand with a raw SDK, and it is the first thing you will open when the agent behaves in a way you did not predict.

Microsoft Agent Framework vs a hand-rolled loop

DimensionHand-rolled loopAgent Framework
Run loopYou write itBuilt in
Tool dispatchYou map names to functionsFrom function signatures
ObservabilityYou add loggingresult.steps trace included
Model accessYou wire a provider keyCopilot SDK chat client
Step cappingYou add a counterRunOptions(max_iterations=...)
Best forFull control, odd edge casesStandard agents, fast

A hand-rolled loop wins only when you need a control flow the framework does not model. For the standard read-tools-then-answer agent, the framework removes the boilerplate that was never the interesting part.

What this does not handle for you

The framework runs the loop; it does not guarantee the answer is right. A tool that returns stale data produces a confident, stale changelog, and the framework will not flag it. It does not sandbox your tools either, so list_merged_prs shelling out to gh runs with whatever permissions your process has, and a tool that writes or deletes needs its own guardrails because the framework will call it exactly as eagerly as it calls a read. The Copilot SDK also ties model access to a subscription and its rate limits, so an agent that loops hard can hit a wall mid-task that has nothing to do with your code. The framework is a loop and a trace, not a substitute for validating the work.

The full working example

python
# release_notes_agent.py -> python release_notes_agent.py
import asyncio
import os
import subprocess
from agent_framework import ChatAgent, RunOptions
from github_copilot_sdk import CopilotChatClient
def list_merged_prs(since: str) -> str:
"""List PRs merged since a date (YYYY-MM-DD) as JSON with
'number', 'title', 'author'."""
out = subprocess.run(
["gh", "pr", "list", "--state", "merged",
"--search", f"merged:>={since}", "--json", "number,title,author"],
capture_output=True, text=True, timeout=30)
return out.stdout or "[]"
agent = ChatAgent(
chat_client=CopilotChatClient(
token=os.environ["GITHUB_COPILOT_TOKEN"], model="gpt-5.5-copilot"),
instructions=(
"You draft release notes. Gather merged PRs with the tool, then "
"write a changelog grouped into Features, Fixes, Chores. Use only "
"PRs the tool returns. Invent nothing."),
tools=[list_merged_prs],
)
async def main():
result = await agent.run(
"Draft release notes for everything merged since 2026-06-15.",
options=RunOptions(max_iterations=8))
for step in result.steps:
if step.tool_call:
print(f"called {step.tool_call.name}({step.tool_call.arguments})")
print("---\n" + result.text)
if __name__ == "__main__":
asyncio.run(main())

Set GITHUB_COPILOT_TOKEN, make sure the gh CLI is authenticated in the repo, and run it. The agent calls the tool, reads the merged PRs, and prints a grouped changelog plus the trace of what it called.

When to reach for this

Use the Agent Framework when you are building a standard agent and want to stop maintaining the loop, the dispatch, and the logging by hand, especially if your team is already on GitHub Copilot and the SDK means model access is a token away. Skip it for a single tool call that does not need a loop at all, and skip it when you need a control flow the framework cannot express. For the common case, a multi-step agent that reads, reasons, and answers, it gets you to a working, observable agent in one file.d