Demo and technical article showing how to connect two Microsoft Agent Framework concepts correctly:
- Agent Harness — the agent control plane (plans, tracks tasks, manages state, requests approvals).
- CodeAct — the execution plane for tool-intensive analysis (loops, aggregations, scoring).
The use case: prioritising a legacy application portfolio for modernisation.
The diagram below shows the full flow from user input to result, end-to-end.
sequenceDiagram
autonumber
actor User
participant Console as Interactive Console
participant Harness as Agent Harness<br/>(control plane)
participant Model as gpt-4o<br/>(Microsoft Foundry)
participant CodeAct as Hyperlight Sandbox<br/>(CodeAct / Python)
participant Tools as C# Tools<br/>(read-only)
participant AppInsights as Application Insights
User->>Console: "Analyse the portfolio and rank applications"
Console->>Harness: Forward prompt + session context
Note over Harness: PLAN MODE
Harness->>Model: Understand intent, build task list
Model-->>Harness: Plan + todos (3-5 tasks)
Harness-->>Console: Show plan to user
Note over Harness: EXECUTE MODE
Harness->>Model: Execute task 1 — retrieve and score portfolio
Model->>CodeAct: execute_code(Python program)
Note over CodeAct: Runs inside Hyperlight micro-VM
CodeAct->>Tools: call_tool("get_applications")
Tools-->>CodeAct: [APP-001, APP-002, APP-003, APP-004]
loop For each application
CodeAct->>Tools: call_tool("get_incidents", applicationId)
Tools-->>CodeAct: IncidentSummary
CodeAct->>Tools: call_tool("get_costs", applicationId)
Tools-->>CodeAct: CostSummary
end
Note over CodeAct: Normalise + score + rank
CodeAct-->>Model: Ranked JSON result
Harness->>AppInsights: OpenTelemetry traces (plan, tools, tokens, duration)
Model->>Harness: Task 2 — explain ranking
Harness-->>Console: Ranking + explanation
User->>Console: "Create a work item for the top application"
Console->>Harness: Forward request
Harness->>Model: Evaluate intent
Model->>Harness: Call create_modernization_work_item (approval required)
Harness-->>Console: ⚠ Approval requested — show details
User->>Console: Approve
Console->>Harness: Approved
Harness->>Tools: create_modernization_work_item(APP-001, ...)
Tools-->>Harness: WorkItemId = MOD-APP-001-001
Harness->>AppInsights: Trace approval event
Harness-->>Console: Work item created
This walkthrough is written for anyone — technical or not. Each step maps to a numbered arrow in the diagram above and explains what is happening, what problem it solves, and why it matters.
"Analyse the portfolio and rank applications"
What this is: a natural-language instruction, not a form, not a script.
What it demonstrates: the agent understands intent. The user does not need to know which systems to query, which formula to apply, or in what order. That is precisely the point — the intelligence gap between a business question and its technical execution is bridged by the agent.
Before doing anything, the agent stops and plans.
The Harness asks the model: "What tasks are needed to answer this?" The model returns a structured task list — three to five steps describing what it intends to do. That list is shown to the user.
What this demonstrates: the agent does not act first and explain later. It proposes a plan and exposes it. This is the governance boundary: a human can see the intent before execution begins.
This is analogous to an employee saying "here is what I am going to do" before starting — not a courtesy, but a control mechanism.
The Harness moves the agent from planning to execution, task by task.
It manages the session (what has been done), the context (what the model can see), and the loop (whether to continue or stop). The model does not manage any of this itself.
What this demonstrates: the model is not the agent. The model reasons. The Harness governs. These are different responsibilities, and separating them is what makes the agent reliable across a long, multi-step task.
For the analysis task, the model writes a short Python program and submits it to the sandbox via execute_code.
That program — in a single unit — does everything:
- Retrieves all four applications.
- Loops over each one.
- Calls
get_incidentsandget_costsper application. - Normalises every dimension to a 0–100 scale.
- Applies the weighted scoring formula.
- Sorts and returns the ranked result.
What this demonstrates: instead of going back and forth between the model and each tool (which is slow, expensive, and hard to audit), the model expresses the entire workflow as a coherent program. The sandbox runs it in one shot.
Think of it as the difference between asking someone a question eight times vs. handing them a clear brief and letting them work through it once.
The Python program runs inside a Hyperlight micro-VM — a hardware-isolated environment. It can only reach the tools it was explicitly given (get_applications, get_incidents, get_costs). It cannot access the network, the file system, credentials, or anything else.
What this demonstrates: isolation is not optional. The model writes code dynamically. That code must run in a controlled space. The sandbox enforces the boundary — the model's program cannot do more than what the tools allow.
Each tool does exactly one thing:
| Tool | Returns |
|---|---|
get_applications |
The four applications — ID, name, technology, age, criticality |
get_incidents |
Severity 1, severity 2, monthly incident count for one app |
get_costs |
Annual run cost and average change lead time for one app |
What this demonstrates: tools are not general-purpose APIs. They are deliberately narrow. A tool like RunSql(string query) or CallApi(string url) would be a back door — the agent could do anything. Narrow contracts are the first line of defence.
The sandbox returns a JSON ranking:
[
{ "applicationId": "APP-001", "name": "Payments Legacy", "score": 100.00 },
{ "applicationId": "APP-002", "name": "Customer Operations", "score": 68.78 },
{ "applicationId": "APP-003", "name": "Portfolio Reporting", "score": 37.83 },
{ "applicationId": "APP-004", "name": "Notification Service", "score": 18.32 }
]What this demonstrates: the output is not an opinion — it is a computed, reproducible result. The same inputs always produce the same ranking. The model then explains the result in natural language, but the numbers come from the algorithm, not from the model's judgement.
While the agent works, the Harness sends structured telemetry to Application Insights: the plan it generated, every tool it called, the tokens it consumed, how long CodeAct took, and when the loop ended.
What this demonstrates: a correct result is not enough. An enterprise needs to know how the result was reached — what the agent did, what evidence it used, what it was not allowed to do. The trace is the audit trail.
"Create a work item for the top application"
What this demonstrates: the agent correctly distinguishes analysis from action. It spent steps 1–8 reading data and computing results. This is the first request to change something in an external system.
Before calling create_modernization_work_item, the agent stops. It shows the user exactly what it is about to do — the application ID, the title, the rationale, the priority score — and waits.
What this demonstrates: write operations are not buried inside the analysis loop. The approval gate is architecturally enforced: the write tool is registered outside the sandbox, with ApprovalRequiredAIFunction wrapping it. The agent cannot skip this step.
This is the critical difference between an AI that does things for you and one that does things with your explicit consent.
The Harness calls create_modernization_work_item, receives WorkItemId = MOD-APP-001-001, and logs the approval event — including who approved it and when.
What this demonstrates: the end state is not just a created ticket. It is a traceable chain from user intent → plan → analysis → approval → action → audit record.
| Not in the demo | Why |
|---|---|
| The model decides the scoring weights | The formula is fixed in code and in the system prompt — the model cannot redefine risk |
| CodeAct creates the work item | Write operations require individual approval; burying them inside a loop would hide them |
| The agent acts without a plan | Plan mode forces intent to be explicit before execution begins |
| The sandbox can access credentials | Tools run in the C# host; the sandbox only calls named, bounded functions |
These are not missing features. They are deliberate architectural boundaries.
| Component | Role | Technology |
|---|---|---|
| Interactive Console | Renders the agent UI (plan/execute mode indicator, todo list, approval prompts) | Harness_Shared_Console (Reactive) |
| Agent Harness | Governs the full task lifecycle: planning, session, context, memory, approvals, loop | Microsoft.Agents.AI.Harness |
| gpt-4o | Reasons, plans, writes CodeAct programs, explains results | Microsoft Foundry (Responses API) |
| Hyperlight Sandbox | Executes the Python program in an isolated micro-VM | Microsoft.Agents.AI.Hyperlight |
| C# Tools | Narrow, auditable read operations — portfolio, incidents, costs | AIFunctionFactory.Create(...) |
| create_modernization_work_item | Write operation with mandatory approval — registered outside CodeAct | ApprovalRequiredAIFunction |
| Application Insights | Receives OpenTelemetry traces: plan, tool calls, token usage, approvals, duration | Log Analytics workspace |
.
├── AgentHarnessCodeAct.slnx
├── .env.example ← env var template
├── global.json
├── infra
│ ├── main.bicep ← Foundry + project + Log Analytics + App Insights
│ ├── model.bicep ← Model deployment (separate)
│ ├── deploy.sh
│ ├── deploy.ps1
│ └── README.md
├── src
│ ├── ModernizationAgent.Core ← Domain model + deterministic scoring
│ └── ModernizationAgent.LocalDemo ← Validates scoring without AI
├── tests
│ └── ModernizationAgent.Tests ← xUnit tests (scoring algorithm baseline)
└── preview
└── HarnessCodeAct
├── Program.cs ← Drop-in for official agent-framework sample
├── README.md
├── apply-to-agent-framework.sh
└── apply-to-agent-framework.ps1
Requires the .NET 10 SDK.
dotnet run --project src/ModernizationAgent.LocalDemoExpected output:
MODERNIZATION RANKING
Pos. Application Technology Score
----------------------------------------------------------------------
1 Payments Legacy COBOL / Mainframe 100.00
2 Customer Operations VB6 / COM+ 68.78
3 Portfolio Reporting .NET Framework 4.6 37.83
4 Notification Service .NET 8 18.32
TOP CANDIDATE DETAIL
Application: Payments Legacy (APP-001)
Final score: 100.00
Scoring breakdown:
- Criticality: 100.00
- Incidents: 100.00
- Cost: 100.00
- Lead time: 100.00
- Age: 100.00
Recommended next step:
Conduct a technical and functional assessment. Work item creation is a separate, subsequent, approvable operation.
What this validates:
- Portfolio tools have narrow, well-defined contracts (
IPortfolioReader). - The scoring algorithm is deterministic (same inputs → same outputs, always).
- The expected ranking is reproducible across environments.
- A write operation (
create_modernization_work_item) is separated from the analysis phase — it is never called by the local demo.
You can also run the xUnit tests to verify the scoring formula deterministically:
dotnet test tests/ModernizationAgent.TestsExpected output: 6 tests passed — four parametric assertions (one per application, matching the expected scores above) plus ordering and count checks.
main.bicep deploys in one shot:
| Resource | Purpose |
|---|---|
Microsoft.CognitiveServices/accounts (AIServices) |
Microsoft Foundry resource |
Microsoft.CognitiveServices/accounts/projects |
Foundry project |
Microsoft.OperationalInsights/workspaces |
Log Analytics workspace for App Insights |
Microsoft.Insights/components |
Application Insights — receives OpenTelemetry traces |
Microsoft.Authorization/roleAssignments |
Foundry User role on the project (optional) |
model.bicep deploys a model on the existing Foundry resource (separately, because model availability varies by region and subscription).
export RESOURCE_GROUP=rg-agent-harness-codeact
export LOCATION=swedencentral
export PRINCIPAL_OBJECT_ID=$(az ad signed-in-user show --query id -o tsv)
./infra/deploy.shOn PowerShell:
$env:RESOURCE_GROUP = "rg-agent-harness-codeact"
$env:LOCATION = "swedencentral"
$env:PRINCIPAL_OBJECT_ID = (az ad signed-in-user show --query id -o tsv)
./infra/deploy.ps1List available models for your region and subscription before deploying one:
az cognitiveservices account list-models \
--name <foundry-name> \
--resource-group <resource-group> \
--query "[].{model:name,format:format,version:version,sku:skus[0].name}" \
--output tableDeploy a model:
az deployment group create \
--resource-group <resource-group> \
--template-file infra/model.bicep \
--parameters \
foundryName=<foundry-name> \
deploymentName=gpt-4o \
modelName=gpt-4o \
modelVersion=2024-11-20 \
modelFormat=OpenAI \
skuName=Standard \
capacity=1The Hyperlight backend requires a Linux host with KVM. The integration runs on top of the official microsoft/agent-framework repository.
git clone https://github.com/microsoft/agent-framework.git
./preview/HarnessCodeAct/apply-to-agent-framework.sh /path/to/agent-frameworkSet environment variables (copy from .env.example):
export FOUNDRY_PROJECT_ENDPOINT="https://<foundry-name>.services.ai.azure.com/api/projects/<project-name>"
export FOUNDRY_MODEL="gpt-4o"Run in an interactive terminal (the console requires a real TTY):
cd agent-framework
dotnet run \
--project dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution \
--framework net10.0Analysis (read-only, runs via CodeAct):
Analyse the full portfolio. Compute the modernisation priority index for each
application, rank the results, and explain why the top application should be
modernised before the others. Do not create any work item yet.
After the agent responds, check the task list:
/todos
Write operation (triggers approval):
Create the work item for the top application with its summary and score.
The agent must pause and request explicit approval because create_modernization_work_item is registered outside CodeAct and wrapped in ApprovalRequiredAIFunction.
| Command | What it does |
|---|---|
/mode |
Toggle between plan mode (think + propose) and execute mode (act) |
/todos |
Show the current task list and completion status |
| Any text | Send a message to the agent |
All agent traces land in Application Insights via OpenTelemetry (source name: ModernizationAgent.HarnessCodeAct).
Open the Logs blade in your Application Insights resource and run these queries.
traces
| where timestamp > ago(1h)
| where isnotempty(customDimensions["AgentName"])
| summarize
Sessions = dcount(tostring(customDimensions["SessionId"])),
TotalTokens = sum(toint(customDimensions["TotalTokens"])),
AvgDuration = avg(duration)
by AgentName = tostring(customDimensions["AgentName"])
| order by Sessions descdependencies
| where timestamp > ago(1h)
| where name == "execute_code"
| project
timestamp,
DurationMs = duration,
Success = success,
SessionId = tostring(customDimensions["SessionId"])
| order by timestamp descdependencies
| where timestamp > ago(1h)
| where name startswith "call_tool"
| summarize
Calls = count(),
AvgDurationMs = avg(duration)
by ToolName = tostring(customDimensions["ToolName"])
| order by Calls desctraces
| where timestamp > ago(24h)
| where message == "Approval granted"
| project
timestamp,
Tool = tostring(customDimensions["ToolName"]),
SessionId = tostring(customDimensions["SessionId"]),
User = tostring(customDimensions["ApprovedBy"])
| order by timestamp desctraces
| where timestamp > ago(1h)
| where isnotempty(customDimensions["PromptTokens"])
| summarize
PromptTokens = sum(toint(customDimensions["PromptTokens"])),
CompletionTokens = sum(toint(customDimensions["CompletionTokens"]))
by bin(timestamp, 5m)
| render timechartexceptions
| where timestamp > ago(1h)
| project timestamp, type, outerMessage, SessionId = tostring(customDimensions["SessionId"])
| order by timestamp descThey are not two separate architectures.
The local demo tests the domain in isolation — no AI, no network, fully deterministic.
The Harness + CodeAct demo tests agentic orchestration:
Harness (control plane) CodeAct (execution plane)
─────────────────────── ─────────────────────────
planning loops over applications
task list (todos) calls get_incidents
session and context calls get_costs
approval requests normalises dimensions
OpenTelemetry traces computes scores
termination criterion produces ranked JSON
| Component | Status | Notes |
|---|---|---|
| .NET 10 | LTS | Stable |
| Agent Harness for .NET | Available | Current official sample targets net10.0 |
| Hyperlight CodeAct for .NET | Preview | Requires Linux host with KVM |
| Hyperlight backend | Preview | Not available on Windows or arbitrary PaaS |
- Tools called via
call_tool(...)run in the C# host process — credentials stay in C#, not in the sandbox. - CodeAct approval applies to the full
execute_codeblock, not to individualcall_toolcalls inside it. That is whycreate_modernization_work_itemis registered directly on the agent, outside CodeAct.