Python: Add a HarnessAgent with available features and sample (#6041)

* Add a HarnessAgent with available features and sample

* Fix formatting

* Address PR comments and fix mypy error

* Add web search support to HarnessAgent

* Fix build warning

* Apply suggestions from code review

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>

* Address PR comments

* Address PR comments

* Address further PR comments.

* Fix markdown broken link

---------

Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
This commit is contained in:
westey
2026-05-27 14:54:00 +01:00
committed by GitHub
Unverified
parent d5c07f2623
commit ef86fb51d5
11 changed files with 1262 additions and 5 deletions
@@ -0,0 +1,83 @@
# Harness Agent Samples
This folder demonstrates `create_harness_agent` — a factory function that builds a
pre-configured, batteries-included agent by assembling the full agent pipeline
from a chat client.
## What is `create_harness_agent`?
`create_harness_agent` bundles the following features into a single `Agent` instance:
| Feature | Description |
|---------|-------------|
| Function invocation | Automatic tool calling loop |
| Per-service-call persistence | History persisted after every model call |
| Compaction | Context-window management (sliding window + tool result compaction) |
| TodoProvider | Todo list management for planning and tracking |
| AgentModeProvider | Plan/execute mode tracking |
| MemoryContextProvider | File-based durable memory (when `memory_store` provided) |
| SkillsProvider | File-based skill discovery and progressive loading |
| OpenTelemetry | Built-in observability |
Each feature can be disabled or customized via keyword arguments.
## Samples
| File | Description |
|------|-------------|
| `harness_research.py` | Interactive research assistant with web search and planning workflow |
## Running
```bash
# Set your Foundry environment variables
export FOUNDRY_PROJECT_ENDPOINT="https://your-project.services.ai.azure.com/api/projects/your-project-name"
export FOUNDRY_MODEL="your-model-deployment-name"
# Authenticate with Azure (required for AzureCliCredential)
az login
# Run the research sample
python samples/02-agents/harness/harness_research.py
```
## Key Concepts
### Minimal Setup
`create_harness_agent` requires only a chat client and token budget parameters:
```python
from agent_framework import create_harness_agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
agent = create_harness_agent(
client=FoundryChatClient(credential=AzureCliCredential()),
max_context_window_tokens=128_000,
max_output_tokens=16_384,
)
```
### Customization
Disable or customize any feature:
```python
agent = create_harness_agent(
client=client,
max_context_window_tokens=128_000,
max_output_tokens=16_384,
name="my-agent",
agent_instructions="Custom instructions here.",
disable_todo=True, # Skip todo management
disable_mode=True, # Skip plan/execute modes
disable_compaction=True, # Skip compaction
)
```
### Plan/Execute Workflow
The `AgentModeProvider` enables a two-phase workflow:
1. **Plan mode** — Interactive: the agent asks questions, creates todos, gets approval
2. **Execute mode** — Autonomous: the agent works through todos independently
@@ -0,0 +1,143 @@
# Copyright (c) Microsoft. All rights reserved.
"""Harness Research Assistant.
Demonstrates ``create_harness_agent`` — a factory function that builds a
pre-configured agent with batteries included, automatically wiring up function
invocation, per-service-call history persistence, compaction, and a rich set of
context providers:
- **TodoProvider** — the agent can create, track, and complete work items
- **AgentModeProvider** — plan/execute mode tracking (interactive vs. autonomous)
- **SkillsProvider** — file-based skill discovery and progressive loading
- **CompactionProvider** — automatic context-window management
- **InMemoryHistoryProvider** — session history with per-service-call persistence
- **OpenTelemetry** — built-in observability via AgentTelemetryLayer
- **Web Search** — real-time web search via ``get_web_search_tool()``
The sample creates a research-focused agent with web search capability and runs
a simple interactive chat loop. The agent will plan research tasks using todos,
switch between plan and execute modes, search the web for current information,
and track its progress.
Special commands:
/exit — End the session.
Environment variables:
FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint URL
FOUNDRY_MODEL — Model deployment name
Authentication:
Run ``az login`` before running this sample.
"""
import asyncio
from agent_framework import create_harness_agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
RESEARCH_INSTRUCTIONS = """\
## Research Assistant Instructions
You are a research assistant. When given a research topic, research it thoroughly using web search and web browsing.
Use your knowledge to form good search queries and hypotheses, but always verify claims with the tools available to you rather than relying on memory alone.
### Research quality
Consult multiple sources when possible and cross-reference key claims.
When sources disagree, note the discrepancy and explain which source you consider more reliable and why.
If a web page fails to load or a search returns irrelevant results, try alternative search queries or sources before moving on.
Track your sources — you will need them when presenting results.
### Presenting results
When presenting your final findings:
- Use Markdown formatting for clarity.
- Use clear sections with headings for each major topic or sub-question.
- Cite your sources inline (e.g., "According to [source name](URL), ...").
- End with a brief summary of key takeaways.
- In addition to returning the results to the user, save the final research report to file memory so it survives compaction and can be referenced later.
"""
async def main() -> None:
load_dotenv()
# Create the chat client.
# For authentication, run `az login` in terminal or replace AzureCliCredential
# with your preferred authentication option.
client = FoundryChatClient(credential=AzureCliCredential())
# Create a harness agent with research-specific instructions.
# All other features (todo, mode, compaction, skills, telemetry, web search) are
# automatically configured with sensible defaults.
agent = create_harness_agent(
client=client,
max_context_window_tokens=128_000,
max_output_tokens=16_384,
name="ResearchAgent",
description="A research assistant that plans and executes research tasks.",
agent_instructions=RESEARCH_INSTRUCTIONS,
)
# Create a session to maintain conversation state across turns.
session = agent.create_session()
print("Research Assistant (powered by create_harness_agent)")
print("=" * 50)
print("Enter a research topic to get started.")
print("Type /exit to end the session.\n")
# Simple interactive chat loop.
while True:
user_input = input("You: ").strip()
if not user_input:
continue
if user_input.lower() == "/exit":
print("\nGoodbye!")
break
# Run the agent with streaming and print the response as it arrives.
print("\nAssistant: ", end="", flush=True)
async for update in agent.run(user_input, session=session, stream=True):
if update.contents:
for content in update.contents:
# Print a brief message for each tool call in the stream.
if content.type == "function_call":
print(f"\n [calling tool: {content.name}]", flush=True)
print(" ", end="", flush=True)
# Show web search activity when the result arrives with action details.
elif content.type in ("search_tool_call", "search_tool_result") and getattr(content, "tool_name", None) == "web_search":
action = None
if content.type == "search_tool_result" and isinstance(content.result, dict):
action = content.result.get("action", {})
elif content.type == "search_tool_call":
action = content.arguments if isinstance(content.arguments, dict) else None
if action:
action_type = action.get("type", "search")
if action_type == "search":
queries = action.get("queries") or []
query_str = ", ".join(f'"{q}"' for q in queries) if queries else action.get("query", "")
print(f"\n 🌐 Web search: {query_str}", flush=True)
print(" ", end="", flush=True)
elif action_type == "open_page":
url = action.get("url", "(unknown)")
print(f"\n 🌐 Opening: {url}", flush=True)
print(" ", end="", flush=True)
elif action_type == "find_in_page":
pattern = action.get("pattern", "")
print(f'\n 🌐 Find in page: "{pattern}"', flush=True)
print(" ", end="", flush=True)
else:
print(f"\n 🌐 Web search: {action_type}", flush=True)
print(" ", end="", flush=True)
# Print text content as it streams in.
if update.text:
print(update.text, end="", flush=True)
print("\n")
if __name__ == "__main__":
asyncio.run(main())