{"id":5187,"date":"2026-03-12T12:30:26","date_gmt":"2026-03-12T19:30:26","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/semantic-kernel\/?p=5187"},"modified":"2026-03-12T15:15:02","modified_gmt":"2026-03-12T22:15:02","slug":"agent-harness-in-agent-framework","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/agent-framework\/agent-harness-in-agent-framework\/","title":{"rendered":"Agent Harness in Agent Framework"},"content":{"rendered":"<p>Agent harness is the layer where model reasoning connects to real execution: shell and filesystem access, approval flows, and context management across long-running sessions. With <em>Agent Framework<\/em>, these patterns can now be built consistently in both Python and .NET.<\/p>\n<p>In this post, we\u2019ll look at three practical building blocks for production agents:<\/p>\n<ul>\n<li><strong>Local shell harness<\/strong> for controlled host-side execution<\/li>\n<li><strong>Hosted shell harness<\/strong> for managed execution environments<\/li>\n<li><strong>Context compaction<\/strong> for keeping long conversations efficient and reliable<\/li>\n<\/ul>\n<h2><strong>Shell and Filesystem Harness<\/strong><\/h2>\n<p>Many agent experiences need to do more than generate text. They need to inspect files, run commands, and work with the surrounding environment in a controlled way. <em>Agent Framework<\/em> makes it possible to model those capabilities explicitly, with approval patterns where needed.<\/p>\n<p>The following examples show compact harness patterns in both Python and .NET.<\/p>\n<p><strong>Python: Local shell with approvals<\/strong><\/p>\n<pre class=\"prettyprint language-py\"><code class=\"language-py\">import asyncio\r\nimport subprocess\r\nfrom typing import Any\r\n\r\nfrom agent_framework import Agent, Message, tool\r\nfrom agent_framework.openai import OpenAIResponsesClient\r\n\r\n\r\n@tool(approval_mode=\"always_require\")\r\ndef run_bash(command: str) -&gt; str:\r\n    \"\"\"Execute a shell command locally and return stdout, stderr, and exit code.\"\"\"\r\n    result = subprocess.run(\r\n        command,\r\n        shell=True,\r\n        capture_output=True,\r\n        text=True,\r\n        timeout=30,\r\n    )\r\n    parts: list[str] = []\r\n    if result.stdout:\r\n        parts.append(result.stdout)\r\n    if result.stderr:\r\n        parts.append(f\"stderr: {result.stderr}\")\r\n    parts.append(f\"exit_code: {result.returncode}\")\r\n    return \"\\n\".join(parts)\r\n\r\n\r\nasync def run_with_approvals(query: str, agent: Agent) -&gt; Any:\r\n    current_input: str | list[Any] = query\r\n\r\n    while True:\r\n        result = await agent.run(current_input)\r\n        if not result.user_input_requests:\r\n            return result\r\n\r\n        next_input: list[Any] = [query]\r\n        for request in result.user_input_requests:\r\n            print(f\"Shell request: {request.function_call.name}\")\r\n            print(f\"Arguments: {request.function_call.arguments}\")\r\n            approved = (await asyncio.to_thread(input, \"Approve command? (y\/n): \")).strip().lower() == \"y\"\r\n            next_input.append(Message(\"assistant\", [request]))\r\n            next_input.append(Message(\"user\", [request.to_function_approval_response(approved)]))\r\n            if not approved:\r\n                return \"Shell command execution was rejected by user.\"\r\n\r\n        current_input = next_input\r\n\r\n\r\nasync def main() -&gt; None:\r\n    client = OpenAIResponsesClient(\r\n        model_id=\"&lt;responses-model-id&gt;\",\r\n        api_key=\"&lt;your-openai-api-key&gt;\",\r\n    )\r\n    local_shell_tool = client.get_shell_tool(func=run_bash)\r\n\r\n    agent = Agent(\r\n        client=client,\r\n        instructions=\"You are a helpful assistant that can run shell commands.\",\r\n        tools=[local_shell_tool],\r\n    )\r\n\r\n    result = await run_with_approvals(\r\n        \"Use run_bash to execute `python --version` and show only stdout.\",\r\n        agent,\r\n    )\r\n    print(result)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())<\/code><\/pre>\n<p>This pattern keeps execution on the host machine while giving the application a clear approval checkpoint before the command runs.<\/p>\n<p><strong>Security note:<\/strong> For local shell execution, we recommend running this logic in an isolated environment and keeping explicit approval in place before commands are allowed to run.<\/p>\n<p><strong>Python: Hosted shell in a managed environment<\/strong><\/p>\n<pre class=\"prettyprint language-py\"><code class=\"language-py\">import asyncio\r\n\r\nfrom agent_framework import Agent\r\nfrom agent_framework.openai import OpenAIResponsesClient\r\n\r\n\r\nasync def main() -&gt; None:\r\n    client = OpenAIResponsesClient(\r\n        model_id=\"&lt;responses-model-id&gt;\",\r\n        api_key=\"&lt;your-openai-api-key&gt;\",\r\n    )\r\n    shell_tool = client.get_shell_tool()\r\n\r\n    agent = Agent(\r\n        client=client,\r\n        instructions=\"You are a helpful assistant that can execute shell commands.\",\r\n        tools=shell_tool,\r\n    )\r\n\r\n    result = await agent.run(\"Use a shell command to show the current date and time\")\r\n    print(result)\r\n\r\n    for message in result.messages:\r\n        shell_calls = [c for c in message.contents if c.type == \"shell_tool_call\"]\r\n        shell_results = [c for c in message.contents if c.type == \"shell_tool_result\"]\r\n\r\n        if shell_calls:\r\n            print(f\"Shell commands: {shell_calls[0].commands}\")\r\n        if shell_results and shell_results[0].outputs:\r\n            for output in shell_results[0].outputs:\r\n                if output.stdout:\r\n                    print(f\"Stdout: {output.stdout}\")\r\n                if output.stderr:\r\n                    print(f\"Stderr: {output.stderr}\")\r\n                if output.exit_code is not None:\r\n                    print(f\"Exit code: {output.exit_code}\")\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())<\/code><\/pre>\n<p>Hosted shell is useful when you want the agent to execute commands in a provider-managed environment rather than directly on the local machine.<\/p>\n<p><strong>.NET: Local shell with approvals<\/strong><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">using System.ComponentModel;\r\nusing System.Diagnostics;\r\nusing Microsoft.Agents.AI;\r\nusing Microsoft.Extensions.AI;\r\nusing OpenAI;\r\n\r\nvar apiKey = \"&lt;your-openai-api-key&gt;\";\r\nvar model = \"&lt;responses-model-id&gt;\";\r\n\r\n[Description(\"Execute a shell command locally and return stdout, stderr and exit code.\")]\r\nstatic string RunBash([Description(\"Bash command to execute.\")] string command)\r\n{\r\n    using Process process = new()\r\n    {\r\n        StartInfo = new ProcessStartInfo\r\n        {\r\n            FileName = \"\/bin\/bash\",\r\n            ArgumentList = { \"-lc\", command },\r\n            RedirectStandardOutput = true,\r\n            RedirectStandardError = true,\r\n            UseShellExecute = false,\r\n        }\r\n    };\r\n\r\n    process.Start();\r\n    process.WaitForExit(30_000);\r\n\r\n    string stdout = process.StandardOutput.ReadToEnd();\r\n    string stderr = process.StandardError.ReadToEnd();\r\n\r\n    return $\"stdout:\\n{stdout}\\nstderr:\\n{stderr}\\nexit_code:{process.ExitCode}\";\r\n}\r\n\r\nIChatClient chatClient = new OpenAIClient(apiKey)\r\n    .GetResponsesClient(model)\r\n    .AsIChatClient();\r\n\r\nAIAgent agent = chatClient.AsAIAgent(\r\n    name: \"LocalShellAgent\",\r\n    instructions: \"Use tools when needed. Avoid destructive commands.\",\r\n    tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(RunBash, name: \"run_bash\"))]);\r\n\r\nAgentSession session = await agent.CreateSessionAsync();\r\nAgentResponse response = await agent.RunAsync(\"Use run_bash to execute `dotnet --version` and return only stdout.\", session);\r\n\r\nList&lt;FunctionApprovalRequestContent&gt; approvalRequests = response.Messages\r\n    .SelectMany(m =&gt; m.Contents)\r\n    .OfType&lt;FunctionApprovalRequestContent&gt;()\r\n    .ToList();\r\n\r\nwhile (approvalRequests.Count &gt; 0)\r\n{\r\n    List&lt;ChatMessage&gt; approvals = approvalRequests\r\n        .Select(request =&gt; new ChatMessage(ChatRole.User, [request.CreateResponse(approved: true)]))\r\n        .ToList();\r\n\r\n    response = await agent.RunAsync(approvals, session);\r\n    approvalRequests = response.Messages\r\n        .SelectMany(m =&gt; m.Contents)\r\n        .OfType&lt;FunctionApprovalRequestContent&gt;()\r\n        .ToList();\r\n}\r\n\r\nConsole.WriteLine(response);<\/code><\/pre>\n<p>Like the Python version, this approach combines local execution with an explicit approval flow so the application stays in control of what actually runs.<\/p>\n<p><strong>Security note:<\/strong> For local shell execution, we recommend running this logic in an isolated environment and keeping explicit approval in place before commands are allowed to run.<\/p>\n<p><strong>.NET: Hosted shell with protocol-level configuration<\/strong><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">using Microsoft.Agents.AI;\r\nusing Microsoft.Extensions.AI;\r\nusing OpenAI;\r\nusing OpenAI.Responses;\r\n\r\nvar apiKey = \"&lt;your-openai-api-key&gt;\";\r\nvar model = \"&lt;responses-model-id&gt;\";\r\n\r\nIChatClient chatClient = new OpenAIClient(apiKey)\r\n    .GetResponsesClient(model)\r\n    .AsIChatClient();\r\n\r\nCreateResponseOptions hostedShellOptions = new();\r\nhostedShellOptions.Patch.Set(\r\n    \"$.tools\"u8,\r\n    BinaryData.FromObjectAsJson(new object[]\r\n    {\r\n        new\r\n        {\r\n            type = \"shell\",\r\n            environment = new\r\n            {\r\n                type = \"container_auto\"\r\n            }\r\n        }\r\n    }));\r\n\r\nAIAgent agent = chatClient\r\n    .AsBuilder()\r\n    .BuildAIAgent(new ChatClientAgentOptions\r\n    {\r\n        Name = \"HostedShellAgent\",\r\n        UseProvidedChatClientAsIs = true,\r\n        ChatOptions = new ChatOptions\r\n        {\r\n            Instructions = \"Use shell commands to answer precisely.\",\r\n            RawRepresentationFactory = _ =&gt; hostedShellOptions\r\n        }\r\n    });\r\n\r\nAgentResponse response = await agent.RunAsync(\"Use a shell command to print UTC date\/time. Return only command output.\");\r\nConsole.WriteLine(response);<\/code><\/pre>\n<p>This makes it possible to target a managed shell environment from .NET today while keeping the rest of the agent flow in the standard Agent Framework programming model.<\/p>\n<h2><strong>Context Compaction<\/strong><\/h2>\n<p>Long-running agent sessions accumulate chat history that can exceed a model&#8217;s context window. The <em>Agent Framework<\/em> includes a built-in compaction system that automatically manages conversation history before each model call \u2014 keeping agents within their token budget without losing important context (<a href=\"https:\/\/learn.microsoft.com\/agent-framework\/agents\/conversations\/compaction\">Docs<\/a>).<\/p>\n<p><strong>Python: In-run compaction on the agent<\/strong><\/p>\n<pre class=\"prettyprint language-py\"><code class=\"language-py\">import asyncio\r\n\r\nfrom agent_framework import Agent, InMemoryHistoryProvider, SlidingWindowStrategy, tool\r\nfrom agent_framework.openai import OpenAIChatClient\r\n\r\n\r\n@tool(approval_mode=\"never_require\")\r\ndef get_weather(city: str) -&gt; str:\r\n    weather_data = {\r\n        \"London\": \"cloudy, 12\u00b0C\",\r\n        \"Paris\": \"sunny, 18\u00b0C\",\r\n        \"Tokyo\": \"rainy, 22\u00b0C\",\r\n    }\r\n    return weather_data.get(city, f\"No data for {city}\")\r\n\r\n\r\nasync def main() -&gt; None:\r\n    client = OpenAIChatClient(\r\n        model_id=\"&lt;chat-model-id&gt;\",\r\n        api_key=\"&lt;your-openai-api-key&gt;\",\r\n    )\r\n\r\n    agent = Agent(\r\n        client=client,\r\n        instructions=\"You are a helpful weather assistant.\",\r\n        tools=[get_weather],\r\n        context_providers=[InMemoryHistoryProvider()],\r\n        compaction_strategy=SlidingWindowStrategy(keep_last_groups=3),\r\n    )\r\n\r\n    session = agent.create_session()\r\n    for query in [\r\n        \"What is the weather in London?\",\r\n        \"How about Paris?\",\r\n        \"And Tokyo?\",\r\n        \"Which city is the warmest?\",\r\n    ]:\r\n        result = await agent.run(query, session=session)\r\n        print(result.text)\r\n\r\n\r\nif __name__ == \"__main__\":\r\n    asyncio.run(main())<\/code><\/pre>\n<p>This example keeps the most recent conversational context intact while trimming older tool-heavy exchanges that no longer need to be replayed in full.<\/p>\n<p><strong>.NET: Compaction pipeline with multiple strategies<\/strong><\/p>\n<pre class=\"prettyprint language-cs language-csharp\"><code class=\"language-cs language-csharp\">using System.ComponentModel;\r\nusing Microsoft.Agents.AI;\r\nusing Microsoft.Agents.AI.Compaction;\r\nusing Microsoft.Extensions.AI;\r\nusing OpenAI;\r\n\r\nvar apiKey = \"&lt;your-openai-api-key&gt;\";\r\nvar model = \"&lt;chat-model-id&gt;\";\r\n\r\n[Description(\"Look up the current price of a product by name.\")]\r\nstatic string LookupPrice([Description(\"The product to look up.\")] string productName) =&gt;\r\n    productName.ToUpperInvariant() switch\r\n    {\r\n        \"LAPTOP\" =&gt; \"The laptop costs $999.99.\",\r\n        \"KEYBOARD\" =&gt; \"The keyboard costs $79.99.\",\r\n        \"MOUSE\" =&gt; \"The mouse costs $29.99.\",\r\n        _ =&gt; $\"No data for {productName}.\"\r\n    };\r\n\r\nIChatClient chatClient = new OpenAIClient(apiKey)\r\n    .GetChatClient(model)\r\n    .AsIChatClient();\r\n\r\nPipelineCompactionStrategy compactionPipeline = new(\r\n    new ToolResultCompactionStrategy(CompactionTriggers.MessagesExceed(7)),\r\n    new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(4)),\r\n    new TruncationCompactionStrategy(CompactionTriggers.GroupsExceed(12)));\r\n\r\nAIAgent agent = chatClient\r\n    .AsBuilder()\r\n    .UseAIContextProviders(new CompactionProvider(compactionPipeline))\r\n    .BuildAIAgent(new ChatClientAgentOptions\r\n    {\r\n        Name = \"ShoppingAssistant\",\r\n        ChatOptions = new ChatOptions\r\n        {\r\n            Instructions = \"You are a concise shopping assistant.\",\r\n            Tools = [AIFunctionFactory.Create(LookupPrice)]\r\n        },\r\n        ChatHistoryProvider = new InMemoryChatHistoryProvider()\r\n    });\r\n\r\nAgentSession session = await agent.CreateSessionAsync();\r\n\r\nstring[] prompts =\r\n[\r\n    \"What's the price of a laptop?\",\r\n    \"How about a keyboard?\",\r\n    \"And a mouse?\",\r\n    \"Which is cheapest?\",\r\n    \"What was the first product I asked about?\"\r\n];\r\n\r\nforeach (string prompt in prompts)\r\n{\r\n    Console.WriteLine($\"User: {prompt}\");\r\n    AgentResponse response = await agent.RunAsync(prompt, session);\r\n    Console.WriteLine($\"Agent: {response}\\\\n\");\r\n\r\n    if (session.TryGetInMemoryChatHistory(out var history))\r\n    {\r\n        Console.WriteLine($\"[Stored message count: {history.Count}]\\\\n\");\r\n    }\r\n}<\/code><\/pre>\n<p>By combining multiple compaction strategies, you can keep sessions responsive and cost-aware without giving up continuity.<\/p>\n<h2><strong>What\u2019s Next?<\/strong><\/h2>\n<p>These patterns make Agent Framework a stronger foundation for real-world agent systems:<\/p>\n<ul>\n<li><strong>Local shell with approvals<\/strong> enables controlled execution on the host.<\/li>\n<li><strong>Hosted shell<\/strong> supports execution in managed environments.<\/li>\n<li><strong>Compaction strategies<\/strong> help long-running sessions stay within limits while preserving useful context.<\/li>\n<\/ul>\n<p>Whether you are building an assistant that can inspect a project workspace or a multi-step workflow that needs durable context over time, these capabilities help close the gap between model reasoning and practical execution.<\/p>\n<p>For more information, check out our <a href=\"https:\/\/learn.microsoft.com\/en-us\/agent-framework\/\">documentation<\/a> and examples on <a href=\"https:\/\/github.com\/microsoft\/agent-framework\">GitHub<\/a>, and install the latest packages from <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Agents.AI\/\">NuGet (.NET)<\/a> or <a href=\"https:\/\/pypi.org\/project\/agent-framework\/\">PyPI (Python)<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Agent harness is the layer where model reasoning connects to real execution: shell and filesystem access, approval flows, and context management across long-running sessions. With Agent Framework, these patterns can now be built consistently in both Python and .NET. In this post, we\u2019ll look at three practical building blocks for production agents: Local shell harness [&hellip;]<\/p>\n","protected":false},"author":156732,"featured_media":5048,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[78,143,34],"tags":[],"class_list":["post-5187","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-net","category-agent-framework","category-python-2"],"acf":[],"blog_post_summary":"<p>Agent harness is the layer where model reasoning connects to real execution: shell and filesystem access, approval flows, and context management across long-running sessions. With Agent Framework, these patterns can now be built consistently in both Python and .NET. In this post, we\u2019ll look at three practical building blocks for production agents: Local shell harness [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/posts\/5187","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/users\/156732"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/comments?post=5187"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/posts\/5187\/revisions"}],"predecessor-version":[{"id":5199,"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/posts\/5187\/revisions\/5199"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/media\/5048"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/media?parent=5187"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/categories?post=5187"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/agent-framework\/wp-json\/wp\/v2\/tags?post=5187"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}