Compare commits

..
53 changed files with 2550 additions and 2082 deletions
+2 -8
View File
@@ -32,13 +32,7 @@ runs:
if grep -q "name = \"$pkg\"" "$f"; then
pkg_dir=$(dirname "$f" | sed 's|python/||')
echo "Excluding workspace package: $pkg ($pkg_dir)"
if awk '/^\[tool\.uv\.workspace\]/{f=1;next} /^\[/{f=0} f && /^exclude = \[/{found=1} END{exit !found}' python/pyproject.toml; then
if ! awk '/^\[tool\.uv\.workspace\]/{f=1;next} /^\[/{f=0} f && /^exclude = \[/ && index($0, "\"'"$pkg_dir"'\"")' python/pyproject.toml | grep -q .; then
sed -i.bak '/\[tool\.uv\.workspace\]/,/^\[/ { /^exclude = \[/ s|\]|, "'"$pkg_dir"'"]| }' python/pyproject.toml
fi
else
sed -i.bak '/\[tool\.uv\.workspace\]/a\exclude = ["'"$pkg_dir"'"]' python/pyproject.toml
fi
sed -i.bak '/\[tool\.uv\.workspace\]/a\exclude = ["'"$pkg_dir"'"]' python/pyproject.toml
sed -i.bak '/'"$pkg"' = { workspace = true }/d' python/pyproject.toml
fi
done
@@ -46,4 +40,4 @@ runs:
- name: Install the project
shell: bash
run: |
cd python && uv sync --all-packages --all-extras --dev --prerelease=if-necessary-or-explicit
cd python && uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit
@@ -0,0 +1,173 @@
# Issue #5350 — Root-cause validation plan
**Issue:** [`.NET: [Bug]: Checkpoint round-trip loses ToolApprovalRequestContent.ToolCall concrete type (FunctionCallContent → base ToolCallContent), breaking FICC approval resume`](https://github.com/microsoft/agent-framework/issues/5350)
**Status of repro:** A focused repro test class
`dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ToolApprovalRequestCheckpointReproTests.cs`
was added covering seven progressively more end-to-end variants of the path the issue
describes — including a **maximal** end-to-end test that uses a real
`ChatClientAgent` driven by `FunctionInvokingChatClient` with an
`ApprovalRequiredAIFunction`. **All seven tests pass** on `main`, consistently across
5 back-to-back runs, i.e. *the bug as described does not reproduce* at any of the
layers exercised here:
| # | Test | What it exercises | Result |
|---|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|
| 1 | `Repro_5350_ToolApprovalRequestContent_DirectJsonMarshallerRoundtrip_PreservesFunctionCallContent` | `JsonMarshaller.Marshal/Marshal<T>` on `ToolApprovalRequestContent` directly (same options chain as `CheckpointManager.CreateJson`) | Pass |
| 2 | `Repro_5350_ToolApprovalRequestContent_WrappedInPortableValue_PreservesFunctionCallContent` | Same, wrapped in a `PortableValue` (as `PortableMessageEnvelope` would store it) | Pass |
| 3 | `Repro_5350_ToolApprovalRequestContent_AsExternalRequestData_PreservesFunctionCallContent` | Same, as the `Data` payload of an `ExternalRequest` | Pass |
| 4 | `Repro_5350_DirectJsonMarshallerRoundtrip_IsDeterministic` | 25× repetition of #1 to rule out flakiness / JIT order | Pass |
| 5 | `Repro_5350_CaptureWireFormat_ForInspection` | Captures the on-the-wire shape so we can compare against the OP's SQL row | Pass |
| 6 | `Repro_5350_EndToEnd_JsonCheckpointResume_PreservesFunctionCallContentAsync` | Full `CheckpointManager.CreateJson(InMemoryJsonStore) → RunStreamingAsync → SuperStep checkpoint → ResumeStreamingAsync` cycle with a `RequestPort<TARC, TARR>` | Pass |
| 7 | `Repro_5350_EndToEnd_ChatClientAgent_WithApprovalRequiredTool_JsonCheckpointResume_PreservesFunctionCallContentAndInvokesToolAsync` | **Maximal**: `ChatClientAgent` over a `MockChatClient` with an `ApprovalRequiredAIFunction`, single-agent `WorkflowBuilder` (no orchestration), `CheckpointManager.CreateJson(InMemoryJsonStore)`. Asserts both that the resumed `RequestInfoEvent.Request` still carries a `FunctionCallContent` AND that approving the request actually invokes the underlying `AIFunction` and lets the workflow continue. | Pass |
This is **consistent with [@lokitoth's second comment](https://github.com/microsoft/agent-framework/issues/5350#issuecomment-4379664401)**:
> Looking at the MEAI types, it does have `[JsonDerivedType(typeof(FunctionCallContent), "functionCall")]` set on it, and has had it that way for some time, so it is unlikely to be the root cause. […] `JsonMarshaller` […] takes an optional `JsonSerializationOptions` provided by the user […] but this is used only if the internal one (via `WorkflowsJsonUtilities`, which chains to `AgentAbstractionsJsonUtilities`, which then chains to the `AIJsonUtilities` class) [is missing the type].
Inspection of the actual wire format produced by `JsonMarshaller` for a
`ToolApprovalRequestContent` containing a `FunctionCallContent` confirms the
discriminator is emitted:
```jsonc
// TARC at top level
{
"toolCall": {
"$type": "functionCall", // ← polymorphism discriminator present
"name": "DoTheThing",
"arguments": { "x": 42 },
"informationalOnly": false,
"callId": "call-1"
},
"requestId": "req-1"
}
// TARC inside a PortableValue (= shape stored by PortableMessageEnvelope)
{
"typeId": {
"assemblyName": "Microsoft.Extensions.AI.Abstractions, Version=10.5.0.0, …",
"typeName": "Microsoft.Extensions.AI.ToolApprovalRequestContent"
},
"value": {
"toolCall": {
"$type": "functionCall",
},
"requestId": "req-1"
}
}
```
So the OP's stated root-cause hypothesis — *"`AIContent`/`ToolCallContent`/
`FunctionCallContent` are missing `[JsonPolymorphic]`/`[JsonDerivedType]`, or
`CheckpointManager.CreateJson` does not pull from `AIJsonUtilities.DefaultOptions`"*
— is **not** what is producing the failure described in the issue. The annotations
exist, the resolver chain wires them through, and the discriminator does survive
both the direct `JsonMarshaller` round-trip and a real `Run → checkpoint → Resume`
cycle for a `RequestPort` whose request type is `ToolApprovalRequestContent`.
The remaining sections lay out, in priority order, the work needed to identify
the *actual* root cause. The plan deliberately keeps the failing repro from
above as the baseline ("this is what works") and walks outward from it toward
the OP's reported scenario, varying one dimension at a time.
## Plan
### Track A — Reproduce in a configuration closer to the OP's pattern "B"
The OP's repro path differs from the new tests in three concrete ways. Tests #7
(maximal repro) and #8#10 (A2 / A3 / A4 below) close all four gaps. The OP's
specific hypothesis (`TARC.ToolCall is not FunctionCallContent` after resume) does
not reproduce in any of them; A2 did however uncover a *separate, unrelated* bug.
| Step | Variable that changes vs. the passing tests in this repo | Why it matters | Outcome |
|------|--------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| A1 | ~~Use `ChatClientAgent` + `ApprovalRequiredAIFunction` bound into a `WorkflowBuilder`~~**covered by test #7 in this PR; passes.** | Was the largest gap to the OP's repro. Closed. | OP hypothesis disproved — `TARC.ToolCall is FunctionCallContent` post-resume, and the tool is actually invoked exactly once when the approval response is sent. |
| A2 | Multi-agent variant: same as #7 but with the agent inside a `GroupChatBuilder` (the OP's actual orchestration) with `RoundRobinGroupChatManager`. | If group chat re-encodes TARC as part of `ChatMessage.Contents` (`AIContent` polymorphism is two-deep through `ToolApprovalRequestContent`), one branch may resolve and the other not. | OP hypothesis disproved — `TARC.ToolCall is FunctionCallContent` post-resume. **But:** sending the approval response surfaces a *different* real bug — `FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponses` throws `ArgumentException: An item with the same key has already been added. Key: ficc_call-1`. Pinned by test as documented misbehavior. |
| A2b | Same as A2 but using `HandoffWorkflowBuilder` (initial agent has the approval tool, a no-op peer agent makes the handoff graph valid; the mock chat client never emits a `handoff_to_*` call). | If the duplicate-key bug from A2 is broader than `RoundRobinGroupChatManager` and lives in the shared `AIAgentHostExecutor` / `ChatProtocolExecutor` path, the handoff workflow should hit it too. | OP hypothesis disproved — `TARC.ToolCall is FunctionCallContent` post-resume **and** the workflow completes cleanly: tool invoked exactly once, zero errors, zero executor failures. The duplicate-key bug from A2 does **not** occur on the handoff path, narrowing it to the group-chat-specific orchestration. |
| A3 | Same as #7 but with the checkpoint `JsonElement` round-tripped through `string` + `JsonDocument.Parse` between commit and retrieve (`StringRoundTripJsonStore`), emulating the SQL `nvarchar` hop in the OP's Dapper store. | The OP uses Dapper + SQL Server. If the column / driver round-trip preserves ordering, this should be identity-preserving — but if it reorders metadata properties, the `$type` discriminator can be moved out of first position, which then requires `AllowOutOfOrderMetadataProperties = true`. | OP hypothesis disproved — byte-preserving string round-trip is identity-preserving for the relevant payload; `TARC.ToolCall is FunctionCallContent` post-resume; tool invoked exactly once. The OP's storage layer would have to *perturb* the JSON (e.g. reorder metadata) for this to reproduce. |
| A4 | Same as #7 but with non-default `JsonSerializerOptions` (`JsonSerializerDefaults.Web`, no AIJsonUtilities resolver) passed as `customOptions` to `CheckpointManager.CreateJson`. | `JsonMarshaller.LookupTypeInfo` only goes to the external options when the internal chain doesn't know about the type. For most cases this won't trigger, but it's worth confirming that supplying a custom `JsonSerializerOptions` does not silently displace the internal chain. | OP hypothesis disproved — custom external options that DO NOT know about `AIContent` types are correctly ignored for known types; the internal `WorkflowsJsonUtilities.DefaultOptions` chain wins. `TARC.ToolCall is FunctionCallContent` post-resume; tool invoked exactly once. |
### Track B — Validate the wire format the OP actually persists
Once Track A has reproduced (or has clearly failed to reproduce) the symptom,
ask the OP for one of the following, in order of preference:
1. The raw `JsonElement.GetRawText()` they pass to their SQL store on commit,
and the raw string they read back on `RetrieveCheckpointAsync` — for the
exact checkpoint that contains the failing `ToolApprovalRequestContent`.
This tells us in a single round-trip whether:
- the `$type` discriminator is present on commit (rules out write-side bug),
- the `$type` discriminator survives the SQL round-trip (rules out store bug),
- the discriminator is in metadata-first position (rules out the
`AllowOutOfOrderMetadataProperties` story).
2. A repro PR/gist that runs against `CheckpointManager.CreateJson(...)` with
an in-memory `JsonCheckpointStore` shim that mirrors how their SQL subclass
passes bytes through. The OP already offered this in the issue body
(*"I can produce a minimal standalone repro against a fake `IChatClient` if useful — let me know."*) — taking them up on it short-circuits a lot of guessing.
### Track C — Defense-in-depth fixes worth landing regardless of root cause
These are addressed in the OP's "Asks" section (asks 24) and are useful
documentation/ergonomics improvements even if the root cause turns out to be in
the OP's store implementation:
1. **Doc-only**: in the XML docs for `CheckpointManager.CreateJson` and on the
`JsonCheckpointStore` base class, explicitly state that the internal options
chain through `WorkflowsJsonUtilities → AgentAbstractionsJsonUtilities →
AIJsonUtilities`, and that user-supplied `customOptions` are consulted only
when the internal chain has no `TypeInfo` for a requested type.
2. **Doc-only**: document the contract that any user `JsonCheckpointStore`
subclass MUST preserve the exact byte sequence of the `JsonElement` it is
given. If the underlying store reorders JSON metadata, the consumer must opt
in by setting `AllowOutOfOrderMetadataProperties = true` on a
`JsonSerializerOptions` passed as `customOptions` to
`CheckpointManager.CreateJson(...)`.
3. **Optional, only if Track A reproduces**: add a regression test mirroring
the failing path under `dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests`
so the fix is locked in.
### Track D — Reject the OP's hypothesis (if not already done)
The combination of:
- the polymorphism annotations existing in `Microsoft.Extensions.AI` for some time
(per @lokitoth),
- the resolver chain in `WorkflowsJsonUtilities.CreateDefaultOptions()` already
putting `AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver` first
(which itself puts `AIJsonUtilities.DefaultOptions.TypeInfoResolver` first),
- the wire format captured above showing `"$type": "functionCall"` is present,
- the full `Run → checkpoint → Resume` test in this PR passing for a plain
`RequestPort<TARC, ToolApprovalResponseContent>` workflow, **and**
- the maximal `ChatClientAgent` + `FunctionInvokingChatClient` +
`ApprovalRequiredAIFunction` test in this PR also passing — including the
assertion that the wrapped `AIFunction` is actually invoked exactly once after
approval and that the workflow then receives the resulting
`FunctionResultContent` and produces a final assistant message,
is sufficient to **disprove** the OP's stated hypothesis. Once Track A or
Track B identifies the actual cause, the issue should be updated with a brief
explanation of why the original guess was incorrect, so future readers don't
re-tread it.
## Pointers for the next investigator
- Repro tests:
`dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/ToolApprovalRequestCheckpointReproTests.cs`
- Marshaller under test:
`dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/JsonMarshaller.cs`
- Where `CheckpointManager.CreateJson` enters that path:
`dotnet/src/Microsoft.Agents.AI.Workflows/CheckpointManager.cs`
- Options chain (TARC ⇒ resolver):
`dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs`
`dotnet/src/Microsoft.Agents.AI.Abstractions/AgentAbstractionsJsonUtilities.cs`
`Microsoft.Extensions.AI.AIJsonUtilities.DefaultOptions`
- `PortableValue`-side serialization (this is where `value.Value.GetType()` is
used on write, which is the most plausible *internal* place a polymorphism
bug could hide, but the wire capture above shows it is not the cause in the
TARC-as-RequestPort scenario):
`dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/PortableValueConverter.cs`
- Likely next code to inspect for Track A1/A2 (agent-host serialization path):
`dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostExecutor*.cs` and
whatever turns the FICC-generated TARC into something
`AIAgentHostExecutor` stores in its state bag.
-1
View File
@@ -112,7 +112,6 @@
<PackageVersion Include="ModelContextProtocol" Version="1.1.0" />
<!-- Hyperlight -->
<PackageVersion Include="Hyperlight.HyperlightSandbox.Api" Version="0.4.0" />
<PackageVersion Include="Hyperlight.HyperlightSandbox.Guest.Python" Version="0.4.0" />
<!-- Inference SDKs -->
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI" Version="0.10.0" />
<PackageVersion Include="Microsoft.ML.Tokenizers" Version="2.0.0" />
-1
View File
@@ -124,7 +124,6 @@
<Project Path="samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step03_DataProcessing/Harness_Step03_DataProcessing.csproj" />
<Project Path="samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveFramework/ConsoleReactiveFramework.csproj" />
<Project Path="samples/02-agents/Harness/ConsoleReactiveComponents/ConsoleReactiveComponents.csproj" />
</Folder>
@@ -24,11 +24,6 @@ public static class AnsiEscapes
/// </summary>
public static string MoveCursor(int row, int column) => $"\x1b[{row};{column}H";
/// <summary>
/// Erases the current line from the cursor position to the end of the line (EL 0).
/// </summary>
public static string EraseToEndOfLine => "\x1b[0K";
/// <summary>
/// Erases the entire current line (EL 2).
/// </summary>
@@ -39,7 +39,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
{
foreach (string line in props.Title.Split('\n'))
{
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
Console.Write(AnsiEscapes.MoveCursor(this.Y + row, this.X));
Console.Write(AnsiEscapes.EraseEntireLine);
Console.Write(line);
row++;
@@ -51,7 +51,7 @@ public class ListSelection : ConsoleReactiveComponent<ListSelectionProps, Consol
for (int i = 0; i < totalItems; i++)
{
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
Console.Write(AnsiEscapes.MoveCursor(this.Y + row, this.X));
Console.Write(AnsiEscapes.EraseEntireLine);
bool isSelected = i == props.SelectedIndex;
@@ -58,11 +58,11 @@ public class TextInput : ConsoleReactiveComponent<TextInputProps, ConsoleReactiv
public override void RenderCore(TextInputProps props, ConsoleReactiveState state)
{
int promptLength = props.Prompt.Length;
int textWidth = props.Width - promptLength;
int textWidth = this.Width - promptLength;
string indent = new(' ', promptLength);
// First line: prompt + start of text
Console.Write(AnsiEscapes.MoveCursor(props.Y, props.X));
Console.Write(AnsiEscapes.MoveCursor(this.Y, this.X));
Console.Write(AnsiEscapes.EraseEntireLine);
Console.Write(props.Prompt);
@@ -90,7 +90,7 @@ public class TextInput : ConsoleReactiveComponent<TextInputProps, ConsoleReactiv
while (offset < props.Text.Length)
{
int chunk = Math.Min(textWidth, props.Text.Length - offset);
Console.Write(AnsiEscapes.MoveCursor(props.Y + row, props.X));
Console.Write(AnsiEscapes.MoveCursor(this.Y + row, this.X));
Console.Write(AnsiEscapes.EraseEntireLine);
Console.Write(indent);
Console.Write(props.Text[offset..(offset + chunk)]);
@@ -17,7 +17,7 @@ public record TextPanelProps : ConsoleReactiveProps
/// <summary>
/// A component that renders a list of pre-rendered string items vertically.
/// Designed for rendering dynamic items in a non-scroll region that may be
/// re-rendered on each update. If the component's <see cref="ConsoleReactiveProps.Height"/>
/// re-rendered on each update. If the component's <see cref="ConsoleReactiveComponent.Height"/>
/// exceeds the number of output lines, leftover lines are erased.
/// </summary>
public class TextPanel : ConsoleReactiveComponent<TextPanelProps, ConsoleReactiveState>
@@ -51,18 +51,18 @@ public class TextPanel : ConsoleReactiveComponent<TextPanelProps, ConsoleReactiv
for (int j = 0; j < lineCount; j++)
{
Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + currentRow));
Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y + currentRow));
Console.Write(lines[j]);
currentRow++;
}
}
// If the component height exceeds the output, erase leftover lines
if (props.Height > currentRow)
if (this.Height > currentRow)
{
for (int i = currentRow; i < props.Height; i++)
for (int i = currentRow; i < this.Height; i++)
{
Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + i));
Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y + i));
}
}
}
@@ -52,7 +52,7 @@ public class TextScrollPanel : ConsoleReactiveComponent<TextScrollPanelProps, Te
}
// Move cursor to the bottom of the scroll area
Console.Write(AnsiEscapes.MoveCursor(props.Y + props.Height - 1, props.X));
Console.Write(AnsiEscapes.MoveCursor(this.Y + this.Height - 1, this.X));
// Output only new items since last rendered
for (int i = state.RenderedCount; i < props.Items.Count; i++)
@@ -9,6 +9,9 @@ namespace Harness.ConsoleReactiveComponents;
/// </summary>
public record TopBottomRuleProps : ConsoleReactiveProps
{
/// <summary>Gets the width of the horizontal rules in characters.</summary>
public int Width { get; init; }
/// <summary>Gets the foreground color of the horizontal rules. If <c>null</c>, the default terminal color is used.</summary>
public ConsoleColor? Color { get; init; }
}
@@ -29,7 +32,7 @@ public class TopBottomRule : ConsoleReactiveComponent<TopBottomRuleProps, Consol
int childrenHeight = 0;
foreach (var child in props.Children)
{
childrenHeight += child.BaseProps?.Height ?? 0;
childrenHeight += child.Height;
}
// Top rule + children + bottom rule
@@ -48,11 +51,11 @@ public class TopBottomRule : ConsoleReactiveComponent<TopBottomRuleProps, Consol
}
// Top rule
Console.Write(AnsiEscapes.MoveCursor(props.Y, props.X));
Console.Write(AnsiEscapes.MoveCursor(this.Y, this.X));
Console.Write(rule);
// Render children stacked below the top rule
int currentY = props.Y + 1;
int currentY = this.Y + 1;
if (props.Color.HasValue)
{
@@ -61,9 +64,10 @@ public class TopBottomRule : ConsoleReactiveComponent<TopBottomRuleProps, Consol
foreach (var child in props.Children)
{
child.BaseProps = child.BaseProps! with { X = props.X, Y = currentY };
child.X = this.X;
child.Y = currentY;
child.Render();
currentY += child.BaseProps.Height;
currentY += child.Height;
}
if (props.Color.HasValue)
@@ -72,7 +76,7 @@ public class TopBottomRule : ConsoleReactiveComponent<TopBottomRuleProps, Consol
}
// Bottom rule
Console.Write(AnsiEscapes.MoveCursor(currentY, props.X));
Console.Write(AnsiEscapes.MoveCursor(currentY, this.X));
Console.Write(rule);
if (props.Color.HasValue)
@@ -3,8 +3,8 @@
namespace Harness.ConsoleReactiveFramework;
/// <summary>
/// Abstract base class for all console UI components. Provides access to layout
/// through <see cref="BaseProps"/> and a <see cref="Render"/> method for drawing to the console.
/// Abstract base class for all console UI components. Provides layout properties
/// (position and size) and a <see cref="Render"/> method for drawing to the console.
/// Derive from <see cref="ConsoleReactiveComponent{TProps, TState}"/> instead of this class directly.
/// </summary>
public abstract class ConsoleReactiveComponent
@@ -13,21 +13,20 @@ public abstract class ConsoleReactiveComponent
{
}
/// <summary>
/// Gets or sets the component's props as the base <see cref="ConsoleReactiveProps"/> type.
/// Used by parent components to set layout (X, Y, Width, Height) on children without
/// knowing the concrete props type.
/// </summary>
public abstract ConsoleReactiveProps? BaseProps { get; set; }
/// <summary>Gets or sets the 1-based column position of the component.</summary>
public int X { get; set; }
/// <summary>Gets or sets the 1-based row position of the component.</summary>
public int Y { get; set; }
/// <summary>Gets or sets the width of the component in columns.</summary>
public int Width { get; set; }
/// <summary>Gets or sets the height of the component in rows.</summary>
public int Height { get; set; }
/// <summary>Renders the component to the console at its current position.</summary>
public abstract void Render();
/// <summary>
/// Invalidates the component's cached render state, causing the next <see cref="Render"/> call
/// to proceed even if props and state have not changed. Use after a screen erase to force repaint.
/// </summary>
public abstract void Invalidate();
}
/// <summary>
@@ -47,13 +46,6 @@ public abstract class ConsoleReactiveComponent<TProps, TState> : ConsoleReactive
/// <summary>Gets or sets the component's props (external configuration).</summary>
public TProps? Props { get; set; }
/// <inheritdoc/>
public override ConsoleReactiveProps? BaseProps
{
get => this.Props;
set => this.Props = (TProps?)value;
}
/// <summary>Gets or sets the component's internal state.</summary>
protected TState? State { get; set; }
@@ -81,8 +73,8 @@ public abstract class ConsoleReactiveComponent<TProps, TState> : ConsoleReactive
return;
}
if (EqualityComparer<TProps>.Default.Equals(this.Props, this._lastRenderedProps)
&& EqualityComparer<TState>.Default.Equals(this.State, this._lastRenderedState))
if (ReferenceEquals(this.Props, this._lastRenderedProps)
&& ReferenceEquals(this.State, this._lastRenderedState))
{
return;
}
@@ -94,16 +86,6 @@ public abstract class ConsoleReactiveComponent<TProps, TState> : ConsoleReactive
}
}
/// <inheritdoc/>
public override void Invalidate()
{
lock (this._renderLock)
{
this._lastRenderedProps = default;
this._lastRenderedState = default;
}
}
/// <summary>
/// Called by <see cref="Render"/> to perform the actual rendering. Override this in derived classes.
/// </summary>
@@ -113,23 +95,11 @@ public abstract class ConsoleReactiveComponent<TProps, TState> : ConsoleReactive
}
/// <summary>
/// Base record for component props. Provides layout properties (position and size)
/// and an optional <see cref="Children"/> collection for composing child components.
/// Base record for component props. Provides an optional <see cref="Children"/> collection
/// for composing child components.
/// </summary>
public record ConsoleReactiveProps
{
/// <summary>Gets the 1-based column position of the component.</summary>
public int X { get; init; }
/// <summary>Gets the 1-based row position of the component.</summary>
public int Y { get; init; }
/// <summary>Gets the width of the component in columns.</summary>
public int Width { get; init; }
/// <summary>Gets the height of the component in rows.</summary>
public int Height { get; init; }
/// <summary>Gets the child components to render within this component.</summary>
public IReadOnlyList<ConsoleReactiveComponent> Children { get; init; } = [];
}
@@ -43,7 +43,7 @@ public class AgentModeAndHelp : ConsoleReactiveComponent<AgentModeAndHelpProps,
}
System.Console.Write(AnsiEscapes.SaveCursor);
System.Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y));
System.Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y));
bool hasMode = props.Mode is not null;
@@ -35,7 +35,6 @@ public class AgentStatus : ConsoleReactiveComponent<AgentStatusProps, AgentStatu
];
private readonly Timer _timer;
private AgentStatusProps? _previousProps;
/// <summary>
/// Initializes a new instance of the <see cref="AgentStatus"/> class.
@@ -86,12 +85,7 @@ public class AgentStatus : ConsoleReactiveComponent<AgentStatusProps, AgentStatu
}
System.Console.Write(AnsiEscapes.SaveCursor);
System.Console.Write(AnsiEscapes.MoveCursor(props.Y, props.X));
if (props != this._previousProps)
{
System.Console.Write(AnsiEscapes.EraseToEndOfLine);
this._previousProps = props;
}
System.Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y));
if (props.ShowSpinner)
{
@@ -371,7 +371,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
};
bottomChildHeight = ListSelection.CalculateHeight(listProps);
listProps = listProps with { Height = bottomChildHeight };
this._listSelection.Height = bottomChildHeight;
this._listSelection.Props = listProps;
bottomChild = this._listSelection;
}
@@ -398,7 +398,8 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
}
bottomChildHeight = TextInput.CalculateHeight(textInputProps, state.ConsoleWidth);
textInputProps = textInputProps with { Width = state.ConsoleWidth, Height = bottomChildHeight };
this._textInput.Width = state.ConsoleWidth;
this._textInput.Height = bottomChildHeight;
this._textInput.Props = textInputProps;
bottomChild = this._textInput;
}
@@ -412,7 +413,8 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
};
bottomChildHeight = TextInput.CalculateHeight(textInputProps, state.ConsoleWidth);
textInputProps = textInputProps with { Width = state.ConsoleWidth, Height = bottomChildHeight };
this._textInput.Width = state.ConsoleWidth;
this._textInput.Height = bottomChildHeight;
this._textInput.Props = textInputProps;
bottomChild = this._textInput;
}
@@ -457,16 +459,6 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
System.Console.Write(AnsiEscapes.EraseScrollbackBuffer);
this._textScrollPanel.Reset();
this._resizedSinceLastRender = false;
// Invalidate all children so they re-render even if props haven't changed
this._rule.Invalidate();
this._textScrollPanel.Invalidate();
this._textPanel.Invalidate();
this._queuedPanel.Invalidate();
this._agentStatus.Invalidate();
this._modeAndHelp.Invalidate();
this._textInput.Invalidate();
this._listSelection.Invalidate();
}
this._scrollRegionBottom = scrollBottom;
@@ -478,35 +470,35 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
? state.ScrollAreaContentItems.Take(state.ScrollAreaContentItems.Count - 1).ToList()
: [];
this._textScrollPanel.X = 1;
this._textScrollPanel.Y = 1;
this._textScrollPanel.Width = state.ConsoleWidth;
this._textScrollPanel.Height = scrollBottom;
this._textScrollPanel.Props = new TextScrollPanelProps
{
X = 1,
Y = 1,
Width = state.ConsoleWidth,
Height = scrollBottom,
Items = scrollItems,
};
this._textScrollPanel.Render();
// Render the text panel for the last (dynamic) item just below the scroll region
this._textPanel.X = 1;
this._textPanel.Y = scrollBottom + 1;
this._textPanel.Width = state.ConsoleWidth;
this._textPanel.Height = textPanelHeight;
this._textPanel.Props = new TextPanelProps
{
X = 1,
Y = scrollBottom + 1,
Width = state.ConsoleWidth,
Height = textPanelHeight,
Items = lastItems,
};
this._textPanel.Render();
// Render queued input items between text panel and agent status
int queuedPanelY = scrollBottom + textPanelHeight + 1;
this._queuedPanel.X = 1;
this._queuedPanel.Y = queuedPanelY;
this._queuedPanel.Width = state.ConsoleWidth;
this._queuedPanel.Height = queuedPanelHeight;
this._queuedPanel.Props = new TextPanelProps
{
X = 1,
Y = queuedPanelY,
Width = state.ConsoleWidth,
Height = queuedPanelHeight,
Items = state.QueuedItems,
};
this._queuedPanel.Render();
@@ -515,41 +507,32 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
int agentStatusY = queuedPanelY + queuedPanelHeight;
if (showStatusAndHelp)
{
this._agentStatus.Props = agentStatusProps with
{
X = 1,
Y = agentStatusY,
Width = state.ConsoleWidth,
Height = agentStatusHeight,
};
this._agentStatus.X = 1;
this._agentStatus.Y = agentStatusY;
this._agentStatus.Width = state.ConsoleWidth;
this._agentStatus.Height = agentStatusHeight;
this._agentStatus.Props = agentStatusProps;
this._agentStatus.Render();
}
// Render the bottom rule + child below the agent status
this._rule.Props = ruleProps with
{
X = 1,
Y = agentStatusY + agentStatusHeight,
};
this._rule.X = 1;
this._rule.Y = agentStatusY + agentStatusHeight;
this._rule.Props = ruleProps;
this._rule.Render();
// Render the mode-and-help line below the bottom rule
if (showStatusAndHelp)
{
int modeAndHelpY = agentStatusY + agentStatusHeight + ruleHeight;
this._modeAndHelp.Props = modeAndHelpProps with
{
X = 1,
Y = modeAndHelpY,
Width = state.ConsoleWidth,
Height = modeAndHelpHeight,
};
int modeAndHelpY = this._rule.Y + ruleHeight;
this._modeAndHelp.X = 1;
this._modeAndHelp.Y = modeAndHelpY;
this._modeAndHelp.Width = state.ConsoleWidth;
this._modeAndHelp.Height = modeAndHelpHeight;
this._modeAndHelp.Props = modeAndHelpProps;
this._modeAndHelp.Render();
}
// Clear the bottom padding line
System.Console.Write(AnsiEscapes.MoveAndEraseLine(state.ConsoleHeight));
// Position cursor for natural typing appearance
this.PositionCursor(state);
}
@@ -563,7 +546,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
int textWidth = state.ConsoleWidth - promptLength;
int textLength = state.InputText.Length;
int textInputY = (this._rule.Props?.Y ?? 0) + 1;
int textInputY = this._rule.Y + 1;
if (textWidth <= 0 || textLength == 0)
{
@@ -581,7 +564,7 @@ public class HarnessAppComponent : ConsoleReactiveComponent<ConsoleReactiveProps
&& state.ListSelectionIndex == state.ListSelectionOptions.Count)
{
int titleLines = state.ListSelectionTitle?.Split('\n').Length ?? 0;
int customOptionY = (this._rule.Props?.Y ?? 0) + 1 + titleLines + state.ListSelectionOptions.Count;
int customOptionY = this._rule.Y + 1 + titleLines + state.ListSelectionOptions.Count;
int cursorCol = 2 + state.ListSelectionCustomInputText.Length + 1;
System.Console.Write(AnsiEscapes.MoveCursor(customOptionY, cursorCol));
}
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using Harness.ConsoleReactiveComponents;
using Microsoft.Agents.AI;
@@ -25,8 +24,6 @@ public static class HarnessConsole
{
options ??= new();
System.Console.OutputEncoding = Encoding.UTF8;
// Null means use defaults; an explicit (possibly empty) list means use exactly what was provided.
var observers = options.Observers
?? HarnessConsoleOptions.BuildDefaultObservers();
@@ -36,9 +33,7 @@ public static class HarnessConsole
var modeProvider = agent.GetService<AgentModeProvider>();
var messageInjector = agent.GetService<MessageInjectingChatClient>();
AgentSession session = options.SessionFactory is not null
? await options.SessionFactory(agent)
: await agent.CreateSessionAsync();
AgentSession session = await agent.CreateSessionAsync();
using var component = new HarnessAppComponent(
placeholder: userPrompt,
@@ -68,7 +63,6 @@ public static class HarnessConsole
System.Console.ResetColor();
System.Console.Write(AnsiEscapes.ResetScrollRegion);
System.Console.Write(AnsiEscapes.EraseScrollbackBuffer);
System.Console.Write(AnsiEscapes.EraseEntireScreen);
System.Console.Write(AnsiEscapes.MoveCursor(1, 1));
System.Console.WriteLine("Goodbye!");
@@ -45,12 +45,6 @@ public class HarnessConsoleOptions
/// </summary>
public Dictionary<string, ConsoleColor> ModeColors { get; set; } = new(DefaultModeColors, StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets an optional factory for creating the <see cref="AgentSession"/>.
/// When <see langword="null"/> (the default), <see cref="AIAgent.CreateSessionAsync"/> is used.
/// </summary>
public Func<AIAgent, Task<AgentSession>>? SessionFactory { get; set; }
/// <summary>
/// Creates the default set of observers without planning support.
/// Includes tool call display, tool approval, error display, reasoning display,
@@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Hyperlight.HyperlightSandbox.Guest.Python" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Harness\Microsoft.Agents.AI.Harness.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hyperlight\Microsoft.Agents.AI.Hyperlight.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="skills\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
@@ -1,122 +0,0 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates a HarnessAgent with ALL features enabled, plus:
// - Hyperlight CodeAct (HyperlightCodeActProvider) for sandboxed Python code execution
// - Skills (AgentSkillsProvider) discovering a local "regex-tester" skill
//
// The agent can plan tasks with todos, manage modes, store memories, read/write files,
// search the web, approve sensitive tools, discover and use skills, and execute arbitrary
// Python code in a Hyperlight sandbox — all pre-configured by the HarnessAgent.
//
// Try asking: "Help me write a regex that matches valid email addresses, then test it."
//
// Special commands:
// /todos — Display the current todo list without invoking the agent.
// /mode — Get or set the current agent mode.
// /exit — End the session.
#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
#pragma warning disable MAAI001 // Suppress experimental API warnings for Agents AI experiments.
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.Identity;
using Harness.Shared.Console;
using HyperlightSandbox.Guest.Python;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hyperlight;
using Microsoft.Extensions.AI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4";
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;
const string TracingSourceName = "Harness.CodeExecution";
// Set up OpenTelemetry tracing that writes spans to a text file.
using var tracerProvider = HarnessTracing.CreateFileTracerProvider(TracingSourceName);
// Create the HyperlightCodeActProvider with the Python/Wasm backend.
// The guest module path is resolved automatically from the Hyperlight.HyperlightSandbox.Guest.Python NuGet package.
using var codeAct = new HyperlightCodeActProvider(
HyperlightCodeActProviderOptions.CreateForWasm(PythonGuestModule.GetModulePath()));
var instructions =
"""
## Technical Assistant Instructions
You are a code-powered technical assistant. You can execute Python code in a sandboxed environment
to solve problems precisely rather than guessing. You also have access to skills that provide
structured workflows for specific technical tasks.
### Code Execution
When a problem requires computation, validation, or testing:
- Write Python code and use `execute_code` to run it in the sandbox.
- Always verify results by running the code rather than reasoning about what would happen.
- If code fails, read the error message carefully, fix the issue, and retry.
### Skills
You have access to discoverable skills. When a task matches a skill's description:
- Follow the skill's instructions carefully.
- Use the skill's reference materials for context.
- Combine the skill's workflow with code execution when appropriate.
### Planning and Research
For complex tasks:
- Break the problem into steps using your todo list.
- Research background information using web search when needed.
- Save important findings to file memory for later reference.
### Presenting Results
- Show your work: include the code you ran and its output.
- Explain what each part of your solution does.
- If applicable, save final results to file memory.
""";
// Create the agent with ALL HarnessAgent features enabled plus Hyperlight CodeAct.
// No Disable* flags are set — TodoProvider, AgentModeProvider, FileMemory, FileAccess,
// ToolApproval, WebSearch, and AgentSkillsProvider are all active.
AIAgent agent =
new AIProjectClient(
new Uri(endpoint),
new DefaultAzureCredential(),
new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) })
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions
{
Name = "CodeExecutionAgent",
Description = "A technical assistant with sandboxed code execution and skill-based workflows.",
OpenTelemetrySourceName = TracingSourceName,
// Point the file memory at a local folder for persistent memory across sessions.
FileMemoryStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "agent-files")),
// Add the HyperlightCodeActProvider so the agent can execute Python code in a sandbox.
AIContextProviders = [codeAct],
ChatOptions = new ChatOptions
{
Instructions = instructions,
MaxOutputTokens = MaxOutputTokens,
Reasoning = new() { Effort = ReasoningEffort.Medium },
},
});
// Run the interactive console session using the shared HarnessConsole helper.
await HarnessConsole.RunAgentAsync(
agent,
userPrompt: "Ask me a technical question, or try: \"Help me write a regex that matches valid email addresses.\"",
new HarnessConsoleOptions
{
Observers = HarnessConsoleOptions.BuildObserversWithPlanning(
agent,
planModeName: "plan",
executionModeName: "execute",
maxContextWindowTokens: MaxContextWindowTokens,
maxOutputTokens: MaxOutputTokens),
CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent),
});
@@ -1,51 +0,0 @@
# Harness Step 04 — Code Execution (Hyperlight + Skills)
This sample demonstrates a HarnessAgent with **all features enabled**, plus:
- **Hyperlight CodeAct** — sandboxed Python code execution via `execute_code` (requires KVM)
- **Skills** — file-based skill discovery (a `regex-tester` skill is included)
The agent can plan tasks, manage modes, store memories, read/write files, search the web, approve sensitive operations, discover and use skills, and execute arbitrary Python code — all pre-configured by the HarnessAgent.
## Prerequisites
- .NET 10 SDK
- An Azure AI Foundry project endpoint
- KVM-capable host (the Hyperlight sandbox runs code in micro-VMs)
## Environment Variables
| Variable | Description |
|----------|-------------|
| `AZURE_AI_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint |
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Model deployment name (default: `gpt-5.4`) |
## Running
```bash
dotnet run
```
## What to Try
- **Regex testing**: "Help me write a regex that matches valid email addresses, then test it against some examples."
- **Code execution**: "Calculate the first 20 prime numbers using the Sieve of Eratosthenes."
- **Skill + code combo**: "I need a regex for ISO 8601 dates — test it thoroughly with edge cases."
## Included Skill
The `skills/regex-tester/` skill instructs the agent to validate regex patterns by executing Python test code in the Hyperlight sandbox. It includes a regex cheatsheet as reference material.
## Features Enabled
| Feature | Description |
|---------|-------------|
| TodoProvider | Task planning and tracking (`/todos` command) |
| AgentModeProvider | Mode switching (`/mode` command) |
| FileMemoryProvider | Persistent memory stored as files |
| FileAccessProvider | Read/write files in a working directory |
| ToolApproval | Don't-ask-again approval for sensitive tools |
| WebSearch | Built-in hosted web search |
| AgentSkillsProvider | Discovers and uses skills from the `skills/` folder |
| HyperlightCodeActProvider | Sandboxed Python execution via `execute_code` |
| OpenTelemetry | Trace logging to a text file |
@@ -1,36 +0,0 @@
---
name: regex-tester
description: Validate, test, and debug regular expressions by executing them against sample inputs. Use when asked to build, verify, or explain a regex pattern.
---
## Usage
When the user asks you to create, validate, or debug a regular expression:
1. **Understand the requirement** — clarify what the pattern should match and what it should reject.
2. **Consult the cheatsheet** — review `references/regex-cheatsheet.md` for syntax reminders if needed.
3. **Write and execute test code** — use the `execute_code` tool to run Python code that:
- Compiles the regex with `re.compile()`
- Tests it against a set of positive examples (should match) and negative examples (should not match)
- Extracts and displays any capturing groups
- Reports pass/fail for each test case
4. **Iterate** — if any test fails, refine the pattern and re-run until all cases pass.
5. **Present the result** — give the user the final pattern, explain what each part does, and show the test results.
## Example Test Script
```python
import re
pattern = re.compile(r'^[\w.+-]+@[\w-]+\.[\w.-]+$')
positives = ["user@example.com", "first.last+tag@sub.domain.org"]
negatives = ["@missing.com", "no-at-sign", "spaces in@address.com"]
for s in positives:
assert pattern.match(s), f"FAIL: expected match for '{s}'"
for s in negatives:
assert not pattern.match(s), f"FAIL: expected no match for '{s}'"
print("All tests passed!")
```
@@ -1,97 +0,0 @@
# Regex Quick Reference (Python `re` module)
## Character Classes
| Pattern | Matches |
|---------|---------|
| `.` | Any character except newline |
| `\d` | Digit `[0-9]` |
| `\D` | Non-digit |
| `\w` | Word character `[a-zA-Z0-9_]` |
| `\W` | Non-word character |
| `\s` | Whitespace `[ \t\n\r\f\v]` |
| `\S` | Non-whitespace |
| `[abc]` | Any of a, b, or c |
| `[^abc]`| Any character except a, b, c |
| `[a-z]` | Range: a through z |
## Quantifiers
| Pattern | Meaning |
|---------|---------|
| `*` | 0 or more (greedy) |
| `+` | 1 or more (greedy) |
| `?` | 0 or 1 (greedy) |
| `{n}` | Exactly n |
| `{n,}` | n or more |
| `{n,m}` | Between n and m |
| `*?`, `+?`, `??` | Non-greedy versions |
## Anchors
| Pattern | Meaning |
|---------|---------|
| `^` | Start of string (or line with `re.MULTILINE`) |
| `$` | End of string (or line with `re.MULTILINE`) |
| `\b` | Word boundary |
| `\B` | Non-word boundary |
## Groups and Backreferences
| Pattern | Meaning |
|---------|---------|
| `(...)` | Capturing group |
| `(?:...)`| Non-capturing group |
| `(?P<name>...)` | Named group |
| `\1` | Backreference to group 1 |
| `(?=...)` | Positive lookahead |
| `(?!...)` | Negative lookahead |
| `(?<=...)` | Positive lookbehind |
| `(?<!...)` | Negative lookbehind |
## Flags
| Flag | Effect |
|------|--------|
| `re.IGNORECASE` / `re.I` | Case-insensitive matching |
| `re.MULTILINE` / `re.M` | `^`/`$` match line boundaries |
| `re.DOTALL` / `re.S` | `.` matches newline |
| `re.VERBOSE` / `re.X` | Allow comments and whitespace |
## Common Patterns
| Use Case | Pattern |
|----------|---------|
| Email (simple) | `^[\w.+-]+@[\w-]+\.[\w.-]+$` |
| IPv4 address | `^\d{1,3}(\.\d{1,3}){3}$` |
| ISO date | `^\d{4}-\d{2}-\d{2}$` |
| URL (http/https) | `^https?://[^\s/$.?#].[^\s]*$` |
| Phone (US) | `^(\+1)?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$` |
## Python API
```python
import re
# Test if a string matches
re.match(r'pattern', "string") # match at start
re.search(r'pattern', "string") # match anywhere
re.fullmatch(r'pattern', "string") # match entire string
# Find all matches
re.findall(r'\d+', "abc 123 def 456") # ['123', '456']
# Named groups
m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', "2025-01-15")
m.group('year') # '2025'
# Replace
re.sub(r'\d+', 'X', "abc 123 def") # 'abc X def'
# Split
re.split(r',+', "a,b,,c") # ['a', 'b', 'c']
# Compile for reuse
pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$')
pattern.match("2025-01-15") # Match object
```
@@ -0,0 +1,838 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows.InProc;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Workflows.UnitTests;
/// <summary>
/// Repro tests for issue #5350: <c>ToolApprovalRequestContent.ToolCall</c> reportedly loses its
/// concrete <see cref="FunctionCallContent"/> type after being persisted via a
/// <c>JsonCheckpointStore</c>-backed <c>CheckpointManager</c> and restored on resume.
///
/// These tests bypass the OP's SQL-backed store and HITL agent setup and directly exercise
/// the same JSON pipeline used by the checkpoint path (<see cref="JsonMarshaller"/> ->
/// <see cref="PortableValueConverter"/> -> <see cref="WorkflowsJsonUtilities"/>.DefaultOptions,
/// which chains through AgentAbstractionsJsonUtilities -> AIJsonUtilities), at progressively
/// more end-to-end layers up to a full <c>CheckpointManager.CreateJson(...)</c> +
/// <c>RunStreamingAsync</c> / <c>ResumeStreamingAsync</c> cycle.
///
/// At the time of writing all of these tests <b>pass</b>, which provides counter-evidence
/// against the root-cause hypothesis stated in the issue body (missing polymorphism
/// metadata / serializer-options chain). See
/// <c>docs/working/issue-5350-root-cause-validation-plan.md</c> for the full investigation
/// plan that builds on this baseline.
/// </summary>
public class ToolApprovalRequestCheckpointReproTests
{
private const string RequestId = "req-1";
private const string CallId = "call-1";
private const string FunctionName = "DoTheThing";
private static FunctionCallContent MakeFunctionCall() => new(
callId: CallId,
name: FunctionName,
arguments: new Dictionary<string, object?> { ["x"] = 42 });
private static ToolApprovalRequestContent MakeApprovalRequest()
=> new(RequestId, MakeFunctionCall());
/// <summary>
/// Direct round-trip of a <see cref="ToolApprovalRequestContent"/> through the same
/// <see cref="JsonMarshaller"/> used by <c>CheckpointManager.CreateJson(...)</c>.
///
/// Per the issue, after deserialization the <c>ToolCall</c> property (declared as the
/// abstract base <c>ToolCallContent</c>) is expected to remain a
/// <see cref="FunctionCallContent"/>. If polymorphism is preserved, this test passes;
/// if the discriminator is dropped on the wire or on read, <c>ToolCall</c> comes back
/// as something other than <see cref="FunctionCallContent"/> and FICC's pattern match
/// (<c>tarc.ToolCall is FunctionCallContent { InformationalOnly: false }</c>) silently
/// skips the approval pair.
/// </summary>
[Fact]
public void Repro_5350_ToolApprovalRequestContent_DirectJsonMarshallerRoundtrip_PreservesFunctionCallContent()
{
ToolApprovalRequestContent original = MakeApprovalRequest();
ToolApprovalRequestContent roundTripped = JsonSerializationTests.RunJsonRoundtrip(original);
roundTripped.Should().NotBeNull();
roundTripped.RequestId.Should().Be(RequestId);
roundTripped.ToolCall.Should().NotBeNull();
// This is the assertion that, per issue #5350, fails on resume.
roundTripped.ToolCall.Should().BeOfType<FunctionCallContent>(
"ToolApprovalRequestContent.ToolCall must round-trip as its concrete FunctionCallContent type, " +
"otherwise FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponses will silently " +
"skip the approval pair after a checkpoint resume (issue #5350).");
FunctionCallContent? fcc = roundTripped.ToolCall as FunctionCallContent;
fcc!.CallId.Should().Be(CallId);
fcc.Name.Should().Be(FunctionName);
}
/// <summary>
/// Same round-trip as above, but wrapped in a <see cref="PortableValue"/>. This more closely
/// mirrors how the workflow runtime stores externally-visible request payloads in a checkpoint
/// (<see cref="PortableMessageEnvelope"/> / <see cref="PortableValue"/>). The
/// <see cref="PortableValueConverter"/> serializes the inner value with
/// <c>marshaller.Marshal(value.Value, value.Value.GetType())</c>, so on the write side the
/// runtime type is used (which should include the discriminator), and on the read side the
/// inner value is materialized as a <see cref="JsonElement"/> then re-deserialized as the
/// declared type via <see cref="PortableValue.As{T}"/>.
/// </summary>
[Fact]
public void Repro_5350_ToolApprovalRequestContent_WrappedInPortableValue_PreservesFunctionCallContent()
{
PortableValue original = new(MakeApprovalRequest());
PortableValue result = JsonSerializationTests.RunJsonRoundtrip(original);
ToolApprovalRequestContent? extracted = result.As<ToolApprovalRequestContent>();
extracted.Should().NotBeNull();
extracted!.RequestId.Should().Be(RequestId);
extracted.ToolCall.Should().NotBeNull();
extracted.ToolCall.Should().BeOfType<FunctionCallContent>(
"PortableValue-wrapped ToolApprovalRequestContent must preserve the concrete " +
"FunctionCallContent on the ToolCall property after checkpoint round-trip (issue #5350).");
}
/// <summary>
/// Round-trip the <see cref="ToolApprovalRequestContent"/> as the payload of an
/// <see cref="ExternalRequest"/>, which is the actual on-the-wire shape for HITL approval
/// requests flowing out of a workflow. This is the closest serializer-only proxy for what
/// happens when the issue reporter calls
/// <c>request.TryGetDataAs&lt;ToolApprovalRequestContent&gt;()</c> after
/// <c>InProcessExecution.ResumeStreamingAsync(...)</c>.
/// </summary>
[Fact]
public void Repro_5350_ToolApprovalRequestContent_AsExternalRequestData_PreservesFunctionCallContent()
{
RequestPort<ToolApprovalRequestContent, ToolApprovalResponseContent> port
= RequestPort.Create<ToolApprovalRequestContent, ToolApprovalResponseContent>("Approval");
ExternalRequest original = ExternalRequest.Create(port, MakeApprovalRequest(), RequestId);
ExternalRequest result = JsonSerializationTests.RunJsonRoundtrip(original);
ToolApprovalRequestContent? extracted = result.Data.As<ToolApprovalRequestContent>();
extracted.Should().NotBeNull();
extracted!.ToolCall.Should().NotBeNull();
extracted.ToolCall.Should().BeOfType<FunctionCallContent>(
"ExternalRequest.Data restored from a JSON checkpoint must preserve the concrete " +
"FunctionCallContent on ToolApprovalRequestContent.ToolCall (issue #5350).");
}
/// <summary>
/// Stability check: run the direct round-trip many times in a row to demonstrate that
/// the failure mode (if present) is deterministic and not a flaky/JIT-order artifact.
/// </summary>
[Fact]
public void Repro_5350_DirectJsonMarshallerRoundtrip_IsDeterministic()
{
for (int i = 0; i < 25; i++)
{
ToolApprovalRequestContent original = MakeApprovalRequest();
ToolApprovalRequestContent roundTripped = JsonSerializationTests.RunJsonRoundtrip(original);
roundTripped.ToolCall.Should().BeOfType<FunctionCallContent>(
$"iteration {i}: ToolCall must consistently round-trip as FunctionCallContent");
}
}
/// <summary>
/// End-to-end checkpoint -> resume repro using the actual <c>CheckpointManager.CreateJson(...)</c>
/// path that the issue reporter uses. A trivial workflow whose entry point is a
/// <c>RequestPort&lt;ToolApprovalRequestContent, ToolApprovalResponseContent&gt;</c> emits a
/// pending external request containing a <see cref="FunctionCallContent"/>. We then:
/// 1. checkpoint the run while the request is pending,
/// 2. resume from the checkpoint via a fresh <c>InProcessExecution</c>,
/// 3. read the re-emitted <see cref="RequestInfoEvent"/>, and
/// 4. assert that <c>request.Data.As&lt;ToolApprovalRequestContent&gt;().ToolCall</c> is
/// still a <see cref="FunctionCallContent"/>.
///
/// This is the closest serializer-and-runtime repro for issue #5350 that does not require a
/// real <c>ChatClientAgent</c> + <c>ApprovalRequiredAIFunction</c>.
/// </summary>
[Fact]
public async Task Repro_5350_EndToEnd_JsonCheckpointResume_PreservesFunctionCallContentAsync()
{
RequestPort<ToolApprovalRequestContent, ToolApprovalResponseContent> requestPort
= RequestPort.Create<ToolApprovalRequestContent, ToolApprovalResponseContent>("ApprovalPort");
ForwardMessageExecutor<ToolApprovalResponseContent> processor = new("Processor");
Workflow workflow = new WorkflowBuilder(requestPort)
.AddEdge(requestPort, processor)
.Build();
CheckpointManager checkpointManager = CheckpointManager.CreateJson(new InMemoryJsonStore());
InProcessExecutionEnvironment env = InProcessExecution.OffThread;
ToolApprovalRequestContent input = MakeApprovalRequest();
CheckpointInfo? checkpoint = null;
ExternalRequest? originalPendingRequest = null;
await using (StreamingRun firstRun = await env.WithCheckpointing(checkpointManager)
.RunStreamingAsync(workflow, input))
{
await foreach (WorkflowEvent evt in firstRun.WatchStreamAsync(blockOnPendingRequest: false))
{
if (evt is RequestInfoEvent requestInfo)
{
originalPendingRequest ??= requestInfo.Request;
}
if (evt is SuperStepCompletedEvent step && step.CompletionInfo?.Checkpoint is { } cp)
{
checkpoint = cp;
}
}
}
originalPendingRequest.Should().NotBeNull("the workflow should have emitted the approval request");
checkpoint.Should().NotBeNull("a checkpoint should have been produced while the request was pending");
// Sanity: the pre-checkpoint payload should be a FunctionCallContent.
ToolApprovalRequestContent? preCheckpoint = originalPendingRequest!.Data.As<ToolApprovalRequestContent>();
preCheckpoint.Should().NotBeNull();
preCheckpoint!.ToolCall.Should().BeOfType<FunctionCallContent>(
"the pre-checkpoint pending request payload must already be a FunctionCallContent");
// Resume from the checkpoint and capture the re-emitted request.
await using StreamingRun resumed = await env.WithCheckpointing(checkpointManager)
.ResumeStreamingAsync(workflow, checkpoint!);
ExternalRequest? resumedPendingRequest = null;
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(10));
await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts.Token))
{
if (evt is RequestInfoEvent requestInfo)
{
resumedPendingRequest ??= requestInfo.Request;
}
}
resumedPendingRequest.Should().NotBeNull("the resumed workflow should re-emit the pending request");
ToolApprovalRequestContent? postResume = resumedPendingRequest!.Data.As<ToolApprovalRequestContent>();
postResume.Should().NotBeNull(
"ExternalRequest.Data.As<ToolApprovalRequestContent>() should materialize the payload after a JSON checkpoint resume");
// The assertion that the issue reporter says fails.
postResume!.ToolCall.Should().NotBeNull();
postResume.ToolCall.Should().BeOfType<FunctionCallContent>(
"after CheckpointManager.CreateJson round-trip via ResumeStreamingAsync, " +
"ToolApprovalRequestContent.ToolCall must still be a FunctionCallContent so that " +
"FunctionInvokingChatClient's pattern match (`tarc.ToolCall is FunctionCallContent`) continues to fire " +
"(issue #5350).");
}
/// <summary>
/// Captures the raw JSON produced for a <see cref="ToolApprovalRequestContent"/> by the
/// checkpoint marshaller. This test always passes; it exists to make the on-the-wire shape
/// visible in test output / debugger when investigating issue #5350 (e.g. to confirm the
/// <c>"$type": "functionCall"</c> discriminator is or is not present for the inner
/// <c>toolCall</c> property).
/// </summary>
[Fact]
public void Repro_5350_CaptureWireFormat_ForInspection()
{
JsonMarshaller marshaller = new();
JsonElement element = marshaller.Marshal(MakeApprovalRequest());
string serialized = element.GetRawText();
// Always-true assertion — purpose of this test is to expose the wire format.
serialized.Should().NotBeNullOrEmpty();
serialized.Should().Contain(CallId, "the call id should be present in the serialized form");
}
/// <summary>
/// Maximal end-to-end repro for issue #5350 using the same shape as the OP's reported
/// scenario (pattern "B" in the GroupChatToolApproval sample), but with a single
/// <see cref="ChatClientAgent"/> bound directly into a <see cref="WorkflowBuilder"/>
/// (no orchestration), and with a real <see cref="ApprovalRequiredAIFunction"/>-wrapped
/// tool that the agent actually attempts to call. The test:
/// <list type="number">
/// <item>builds a <see cref="ChatClientAgent"/> over a <see cref="MockChatClient"/> that
/// returns a <see cref="FunctionCallContent"/> on the first turn and a final assistant
/// text on the second turn (so <see cref="FunctionInvokingChatClient"/> converts the FCC
/// to a <see cref="ToolApprovalRequestContent"/> and surfaces it as a workflow
/// <see cref="RequestInfoEvent"/>),</item>
/// <item>persists checkpoints via the OP's exact path —
/// <c>CheckpointManager.CreateJson(InMemoryJsonStore)</c> +
/// <c>InProcessExecutionEnvironment.WithCheckpointing(...).RunStreamingAsync(...)</c> —
/// so every checkpoint is round-tripped through the same <see cref="JsonMarshaller"/> +
/// <see cref="PortableValueConverter"/> pipeline the OP's SQL-backed store uses,</item>
/// <item>validates that the first-run <see cref="RequestInfoEvent.Request"/> carries a
/// <see cref="ToolApprovalRequestContent"/> whose <c>ToolCall</c> is a
/// <see cref="FunctionCallContent"/>,</item>
/// <item>disposes the run and resumes from the last <see cref="SuperStepCompletedEvent"/>
/// checkpoint via <see cref="InProcessExecutionEnvironment.ResumeStreamingAsync"/>,</item>
/// <item>validates the re-emitted <see cref="RequestInfoEvent.Request"/> still carries a
/// <see cref="ToolApprovalRequestContent"/> whose <c>ToolCall</c> is a
/// <see cref="FunctionCallContent"/> — this is the assertion the OP claims fails,</item>
/// <item>sends an approval response back into the resumed run and asserts the wrapped
/// function is actually invoked (counter increments) and the workflow completes with a
/// final assistant message.</item>
/// </list>
/// </summary>
[Fact]
public async Task Repro_5350_EndToEnd_ChatClientAgent_WithApprovalRequiredTool_JsonCheckpointResume_PreservesFunctionCallContentAndInvokesToolAsync()
{
// Arrange — counting tool wrapped for approval
int invocationCount = 0;
const string ToolName = "GetWeather";
const string ToolResultText = "Sunny, 22°C";
AIFunction underlyingTool = AIFunctionFactory.Create(
([Description("City to look up")] string city) =>
{
Interlocked.Increment(ref invocationCount);
return ToolResultText;
},
name: ToolName,
description: "Gets the weather for the given city");
ApprovalRequiredAIFunction approvalTool = new(underlyingTool);
// Arrange — mock chat client that turn-1 emits an FCC for the approval-required tool,
// turn-2 (after FunctionInvokingChatClient processes the approval + invokes the tool +
// appends a FunctionResultContent) emits a final assistant text.
const string ToolCallId = "call-1";
const string FinalAssistantText = "The weather in Amsterdam is sunny and 22°C.";
int chatCallIndex = 0;
List<List<ChatMessage>> capturedInputs = new();
MockChatClient mockChatClient = new((messages, options) =>
{
// Capture a snapshot of the inputs the agent passed in for this service call so the
// test can later assert the FunctionResultContent flowed back to the model.
capturedInputs.Add(new List<ChatMessage>(messages));
int index = Interlocked.Increment(ref chatCallIndex) - 1;
return index switch
{
0 => new ChatResponse(new ChatMessage(ChatRole.Assistant,
[new FunctionCallContent(
callId: ToolCallId,
name: ToolName,
arguments: new Dictionary<string, object?> { ["city"] = "Amsterdam" })])),
_ => new ChatResponse(new ChatMessage(ChatRole.Assistant, FinalAssistantText)),
};
});
ChatClientAgent agent = new(
mockChatClient,
instructions: "You are a weather agent.",
name: "WeatherAgent",
tools: [approvalTool]);
// Arrange — single-agent workflow. The AIAgent is auto-promoted to an ExecutorBinding
// via the implicit operator on ExecutorBinding.
Workflow workflow = new WorkflowBuilder(agent).Build();
// Arrange — JSON checkpoint manager backed by an in-memory JSON store. This mirrors
// the OP's "JsonCheckpointStore-backed CheckpointManager.CreateJson(...)" path —
// every checkpoint is round-tripped through the same JsonMarshaller +
// PortableValueConverter that the OP's SQL-backed store uses, just without the
// disk/SQL hop.
CheckpointManager checkpointManager = CheckpointManager.CreateJson(new InMemoryJsonStore());
InProcessExecutionEnvironment env = InProcessExecution.OffThread;
List<ChatMessage> inputMessages = [new(ChatRole.User, "What's the weather in Amsterdam?")];
// Act 1 — run until we see the approval request, then capture the latest checkpoint.
ExternalRequest? firstRunRequest = null;
CheckpointInfo? checkpoint = null;
await using (StreamingRun firstRun = await env.WithCheckpointing(checkpointManager)
.RunStreamingAsync(workflow, inputMessages))
{
// Trigger an actual turn — without a TurnToken the AIAgentHostExecutor will not
// invoke the agent. This matches the GroupChatToolApproval sample and the
// StreamAsyncWithTurnTokenShouldExecuteWorkflow pattern in InProcessExecutionTests.
(await firstRun.TrySendMessageAsync(new TurnToken(emitEvents: false)))
.Should().BeTrue("the workflow should accept a TurnToken");
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
await foreach (WorkflowEvent evt in firstRun.WatchStreamAsync(blockOnPendingRequest: false, cts.Token))
{
if (evt is RequestInfoEvent requestInfo)
{
firstRunRequest ??= requestInfo.Request;
}
if (evt is SuperStepCompletedEvent step && step.CompletionInfo?.Checkpoint is { } cp)
{
checkpoint = cp;
}
}
}
firstRunRequest.Should().NotBeNull(
"the ChatClientAgent + FICC pipeline should have surfaced the approval request as a workflow RequestInfoEvent");
checkpoint.Should().NotBeNull(
"a checkpoint should have been produced while the approval request was pending");
chatCallIndex.Should().Be(1, "the mock chat client should have been called exactly once before the approval was requested");
invocationCount.Should().Be(0, "the underlying tool must NOT have been invoked before the approval was granted");
ToolApprovalRequestContent? preCheckpoint = firstRunRequest!.Data.As<ToolApprovalRequestContent>();
preCheckpoint.Should().NotBeNull("the pending external request should carry a ToolApprovalRequestContent payload");
preCheckpoint!.ToolCall.Should().BeOfType<FunctionCallContent>(
"the pre-checkpoint pending request payload must already be a FunctionCallContent");
// Act 2 — resume from the checkpoint with a brand-new env / handle so that any
// in-process AIAgentHostExecutor instance state is gone and everything has to be
// rehydrated from the on-disk JSON.
ExternalRequest? resumedRequest = null;
List<WorkflowEvent> postResumeEvents = [];
await using (StreamingRun resumed = await env.WithCheckpointing(checkpointManager)
.ResumeStreamingAsync(workflow, checkpoint!))
{
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
// First pass: see the re-emitted RequestInfoEvent, but don't block on it.
await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts.Token))
{
if (evt is RequestInfoEvent requestInfo)
{
resumedRequest ??= requestInfo.Request;
}
}
resumedRequest.Should().NotBeNull(
"the resumed workflow should re-emit the pending approval RequestInfoEvent");
// The core issue #5350 assertion.
ToolApprovalRequestContent? postResume = resumedRequest!.Data.As<ToolApprovalRequestContent>();
postResume.Should().NotBeNull(
"ExternalRequest.Data.As<ToolApprovalRequestContent>() should materialize the payload after a JSON-file checkpoint resume");
postResume!.ToolCall.Should().NotBeNull("the resumed TARC must carry its ToolCall");
postResume.ToolCall.Should().BeOfType<FunctionCallContent>(
"after CheckpointManager.CreateJson(InMemoryJsonStore) round-trip via " +
"ResumeStreamingAsync, ToolApprovalRequestContent.ToolCall must still be a " +
"FunctionCallContent so that FunctionInvokingChatClient's pattern match " +
"(`tarc.ToolCall is FunctionCallContent`) continues to fire (issue #5350).");
FunctionCallContent resumedFcc = (FunctionCallContent)postResume.ToolCall;
resumedFcc.Name.Should().Be(ToolName);
resumedFcc.CallId.Should().EndWith(ToolCallId,
"the workflow rewrites the CallId with an executor-scoped prefix, but should preserve the original tail");
// Act 3 — send the approval response back into the resumed run and watch the
// remaining stream. This drives FunctionInvokingChatClient through its
// post-approval branch, where it must invoke the underlying AIFunction, append a
// FunctionResultContent, and call the model a second time.
ToolApprovalResponseContent approvalResponse = postResume.CreateResponse(approved: true);
await resumed.SendResponseAsync(resumedRequest.CreateResponse(approvalResponse));
using CancellationTokenSource cts2 = new(TimeSpan.FromSeconds(30));
await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts2.Token))
{
postResumeEvents.Add(evt);
}
}
// Assert 3 — the tool actually got called as part of the approval round-trip, and
// the workflow continued without raising errors.
invocationCount.Should().Be(1,
"approving the request should cause FunctionInvokingChatClient to invoke the wrapped AIFunction exactly once");
chatCallIndex.Should().Be(2,
"after the tool was invoked, FunctionInvokingChatClient should have made a second chat-client call to produce the final assistant message");
capturedInputs.Should().HaveCount(2);
capturedInputs[1].Should().Contain(
m => m.Contents.OfType<FunctionResultContent>().Any(),
"the second chat-client call must include the FunctionResultContent produced by the approved tool invocation");
postResumeEvents.OfType<WorkflowErrorEvent>().Should().BeEmpty(
"no workflow errors should be raised when responding to the resumed approval request");
postResumeEvents.OfType<ExecutorFailedEvent>().Should().BeEmpty(
"no executor failures should be raised when responding to the resumed approval request");
}
/// <summary>
/// Minimal <see cref="IChatClient"/> stub for repro tests; delegates each call to a caller-supplied factory.
/// </summary>
private sealed class MockChatClient(Func<IEnumerable<ChatMessage>, ChatOptions?, ChatResponse> responseFactory) : IChatClient
{
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
=> Task.FromResult(responseFactory(messages, options));
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ChatResponse response = await this.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false);
foreach (ChatResponseUpdate update in response.ToChatResponseUpdates())
{
yield return update;
}
}
public object? GetService(Type serviceType, object? serviceKey = null) => null;
public void Dispose() { }
}
/// <summary>
/// Track A2 — same as the maximal repro (test #7) but with the agent inside a
/// <c>GroupChatBuilder</c> (round-robin manager with a single participant), to rule out
/// a group-chat-specific re-wrap or replay path that drops the
/// <see cref="ToolApprovalRequestContent.ToolCall"/> type during the checkpoint round-trip.
///
/// <para><b>Finding:</b> the <see cref="FunctionCallContent"/> type IS preserved after
/// resume (consistent with tests #1 #7 — the OP's hypothesis still does not reproduce
/// in this configuration). However, sending an approval response back into the resumed
/// group-chat workflow surfaces a <i>different</i>, real bug:
/// <c>FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponses</c> throws
/// <see cref="ArgumentException"/> with message
/// <c>"An item with the same key has already been added. Key: ficc_call-1"</c>.
/// This test pins that observed behavior so future investigation can latch onto it.</para>
/// </summary>
[Fact]
public async Task Repro_5350_A2_GroupChatBuilder_WithApprovalRequiredTool_JsonCheckpointResume_PreservesFunctionCallContentAsync()
{
ReproHarness harness = new();
Workflow workflow = AgentWorkflowBuilder
.CreateGroupChatBuilderWith(agents => new RoundRobinGroupChatManager(agents) { MaximumIterationCount = 4 })
.AddParticipants(harness.Agent)
.Build();
await RunReproAsync(
workflow,
harness,
CheckpointManager.CreateJson(new InMemoryJsonStore()),
scenarioName: "A2 (group chat)",
expectCleanPostApprovalCompletion: false,
postApprovalCompletionAssertion: events =>
{
// A2 currently surfaces a duplicate-key crash inside FICC when the group-chat
// workflow resumes with a pending approval request and the response arrives.
// This assertion pins the observed misbehavior. If the upstream
// GroupChat + AIAgentHostExecutor + FICC interaction is fixed, this test will
// start failing here — that's intentional. The fix should then either remove
// this assertion or convert it to a clean-completion assertion mirroring
// test #7. TODO(#5350-followup): file a separate issue for the duplicate-key
// crash if one does not already exist, and link it here.
ArgumentException? duplicateKey = events
.OfType<WorkflowErrorEvent>()
.Select(e => e.Data as Exception)
.Select(e => e?.InnerException as ArgumentException ?? e as ArgumentException)
.FirstOrDefault(e => e?.Message.Contains("ficc_call-1", StringComparison.Ordinal) == true);
duplicateKey.Should().NotBeNull(
"[A2 (group chat)] approving the resumed approval request currently surfaces a duplicate-key " +
"ArgumentException out of FunctionInvokingChatClient.ExtractAndRemoveApprovalRequestsAndResponses — " +
"this is a real bug, but it is NOT the bug claimed in issue #5350. The TARC.ToolCall was already " +
"asserted to be a FunctionCallContent above; the OP's hypothesis remains unreproduced.");
});
}
/// <summary>
/// Track A2b — same as A2, but using a <c>HandoffWorkflowBuilder</c> instead of a group
/// chat. The initial agent is the same approval-tool-equipped <see cref="ChatClientAgent"/>
/// as test #7; a second dummy agent is registered solely so the handoff graph has a valid
/// peer (the mock chat client never emits a <c>handoff_to_*</c> call, so the workflow stays
/// on the initial agent). This isolates whether anything in the handoff-specific
/// orchestration (handoff tool injection, <c>HandoffMessagesFilter</c>, the
/// <c>HandoffStartExecutor</c>/<c>HandoffEndExecutor</c> wrap-up) perturbs the
/// <see cref="ToolApprovalRequestContent"/> payload across a JSON checkpoint resume.
///
/// <para><b>Finding:</b> the handoff path is fully clean. The <see cref="FunctionCallContent"/>
/// type is preserved after resume <i>and</i> approving the resumed request completes the
/// workflow without errors and invokes the wrapped <see cref="AIFunction"/> exactly once.
/// This is in contrast to A2 (group chat), which preserves the type but crashes on
/// approval with a duplicate-key <see cref="ArgumentException"/> in FICC. The OP's
/// hypothesis (<c>TARC.ToolCall is not FunctionCallContent</c>) still does not reproduce.
/// </para>
/// </summary>
[Fact]
public async Task Repro_5350_A2b_HandoffWorkflowBuilder_WithApprovalRequiredTool_JsonCheckpointResume_PreservesFunctionCallContentAndInvokesToolAsync()
{
ReproHarness harness = new();
// Second agent is a no-op peer required to give the handoff graph a valid target.
// The mock chat client only ever emits a FunctionCallContent for GetWeather, so the
// initial agent never actually hands off; the second agent is never invoked.
MockChatClient peerChatClient = new((messages, options) =>
new ChatResponse(new ChatMessage(ChatRole.Assistant, "(unused peer)")));
ChatClientAgent peerAgent = new(
peerChatClient,
instructions: "Unused peer agent.",
name: "PeerAgent");
Workflow workflow = AgentWorkflowBuilder
.CreateHandoffBuilderWith(harness.Agent)
.WithHandoff(harness.Agent, peerAgent)
.Build();
await RunReproAsync(
workflow,
harness,
CheckpointManager.CreateJson(new InMemoryJsonStore()),
scenarioName: "A2b (handoff)");
}
/// <summary>
/// Track A3 — same as the maximal repro (test #7) but the checkpoint <see cref="JsonElement"/>
/// is round-tripped through <see cref="JsonElement.GetRawText"/> + <see cref="JsonDocument.Parse(string,JsonDocumentOptions)"/>
/// between commit and retrieve, to emulate the SQL <c>nvarchar</c> / Dapper hop in the
/// OP's setup. This catches cases where the storage layer would only fail if it perturbed
/// the JSON in some way (e.g. dropped metadata, re-encoded numbers), and confirms a
/// byte-preserving string round-trip on its own is harmless.
/// </summary>
[Fact]
public async Task Repro_5350_A3_StringRoundTripStore_PreservesFunctionCallContentAsync()
{
ReproHarness harness = new();
Workflow workflow = new WorkflowBuilder(harness.Agent).Build();
CheckpointManager checkpointManager = CheckpointManager.CreateJson(
new StringRoundTripJsonStore(new InMemoryJsonStore()));
await RunReproAsync(
workflow,
harness,
checkpointManager,
scenarioName: "A3 (string round-trip store)");
}
/// <summary>
/// Track A4 — same as the maximal repro (test #7) but with a non-default
/// <see cref="JsonSerializerOptions"/> passed as <c>customOptions</c> to
/// <see cref="CheckpointManager.CreateJson"/>. The custom options intentionally do NOT
/// include the polymorphism resolver chain (<c>AgentAbstractionsJsonUtilities</c> →
/// <c>AIJsonUtilities</c>) — confirming that <c>JsonMarshaller</c>'s internal
/// <c>WorkflowsJsonUtilities.DefaultOptions</c> chain always wins for known
/// <see cref="AIContent"/> types and the external options cannot silently displace it.
/// </summary>
[Fact]
public async Task Repro_5350_A4_CustomJsonSerializerOptions_PreservesFunctionCallContentAsync()
{
ReproHarness harness = new();
Workflow workflow = new WorkflowBuilder(harness.Agent).Build();
// Custom options that DO NOT know about AIContent / FunctionCallContent.
JsonSerializerOptions customOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
};
CheckpointManager checkpointManager = CheckpointManager.CreateJson(
new InMemoryJsonStore(),
customOptions);
await RunReproAsync(
workflow,
harness,
checkpointManager,
scenarioName: "A4 (custom JsonSerializerOptions)");
}
/// <summary>
/// Shared end-to-end repro driver used by tests #7, A2, A3, A4.
/// Drives the workflow until an approval request appears, captures the latest checkpoint,
/// disposes the run, resumes from the checkpoint, and asserts that the resumed
/// <see cref="RequestInfoEvent"/>'s <see cref="ToolApprovalRequestContent.ToolCall"/> is
/// still a <see cref="FunctionCallContent"/>. Then sends an approval response and asserts
/// the wrapped <see cref="AIFunction"/> is invoked exactly once.
/// </summary>
private static async Task RunReproAsync(
Workflow workflow,
ReproHarness harness,
CheckpointManager checkpointManager,
string scenarioName,
bool expectCleanPostApprovalCompletion = true,
Action<IReadOnlyList<WorkflowEvent>>? postApprovalCompletionAssertion = null)
{
InProcessExecutionEnvironment env = InProcessExecution.OffThread;
List<ChatMessage> inputMessages = [new(ChatRole.User, "What's the weather in Amsterdam?")];
// Act 1 — run until approval request appears and capture latest checkpoint.
ExternalRequest? firstRunRequest = null;
CheckpointInfo? checkpoint = null;
await using (StreamingRun firstRun = await env.WithCheckpointing(checkpointManager)
.RunStreamingAsync(workflow, inputMessages))
{
(await firstRun.TrySendMessageAsync(new TurnToken(emitEvents: false)))
.Should().BeTrue($"[{scenarioName}] the workflow should accept a TurnToken");
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
await foreach (WorkflowEvent evt in firstRun.WatchStreamAsync(blockOnPendingRequest: false, cts.Token))
{
if (evt is RequestInfoEvent requestInfo)
{
firstRunRequest ??= requestInfo.Request;
}
if (evt is SuperStepCompletedEvent step && step.CompletionInfo?.Checkpoint is { } cp)
{
checkpoint = cp;
}
}
}
firstRunRequest.Should().NotBeNull(
$"[{scenarioName}] the ChatClientAgent + FICC pipeline should surface the approval request as a workflow RequestInfoEvent");
checkpoint.Should().NotBeNull(
$"[{scenarioName}] a checkpoint should have been produced while the approval request was pending");
harness.ChatCallCount.Should().Be(1, $"[{scenarioName}] the mock chat client should have been called exactly once before approval was requested");
harness.InvocationCount.Should().Be(0, $"[{scenarioName}] the underlying tool must NOT have been invoked before approval was granted");
ToolApprovalRequestContent? preCheckpoint = firstRunRequest!.Data.As<ToolApprovalRequestContent>();
preCheckpoint.Should().NotBeNull($"[{scenarioName}] the pending external request should carry a ToolApprovalRequestContent payload");
preCheckpoint!.ToolCall.Should().BeOfType<FunctionCallContent>(
$"[{scenarioName}] the pre-checkpoint pending request payload must already be a FunctionCallContent");
// Act 2 — resume from checkpoint on a fresh handle.
ExternalRequest? resumedRequest = null;
List<WorkflowEvent> postResumeEvents = [];
await using (StreamingRun resumed = await env.WithCheckpointing(checkpointManager)
.ResumeStreamingAsync(workflow, checkpoint!))
{
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30));
await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts.Token))
{
if (evt is RequestInfoEvent requestInfo)
{
resumedRequest ??= requestInfo.Request;
}
}
resumedRequest.Should().NotBeNull(
$"[{scenarioName}] the resumed workflow should re-emit the pending approval RequestInfoEvent");
ToolApprovalRequestContent? postResume = resumedRequest!.Data.As<ToolApprovalRequestContent>();
postResume.Should().NotBeNull(
$"[{scenarioName}] ExternalRequest.Data.As<ToolApprovalRequestContent>() should materialize the payload after JSON-checkpoint resume");
postResume!.ToolCall.Should().NotBeNull($"[{scenarioName}] the resumed TARC must carry its ToolCall");
postResume.ToolCall.Should().BeOfType<FunctionCallContent>(
$"[{scenarioName}] after CheckpointManager.CreateJson round-trip via ResumeStreamingAsync, " +
"ToolApprovalRequestContent.ToolCall must still be a FunctionCallContent so that " +
"FunctionInvokingChatClient's pattern match (`tarc.ToolCall is FunctionCallContent`) continues to fire (issue #5350).");
// Act 3 — approve and finish the workflow.
ToolApprovalResponseContent approvalResponse = postResume.CreateResponse(approved: true);
await resumed.SendResponseAsync(resumedRequest.CreateResponse(approvalResponse));
using CancellationTokenSource cts2 = new(TimeSpan.FromSeconds(30));
await foreach (WorkflowEvent evt in resumed.WatchStreamAsync(blockOnPendingRequest: false, cts2.Token))
{
postResumeEvents.Add(evt);
}
}
if (expectCleanPostApprovalCompletion)
{
harness.InvocationCount.Should().Be(1,
$"[{scenarioName}] approving the request should cause FunctionInvokingChatClient to invoke the wrapped AIFunction exactly once");
postResumeEvents.OfType<WorkflowErrorEvent>().Should().BeEmpty(
$"[{scenarioName}] no workflow errors should be raised when responding to the resumed approval request");
postResumeEvents.OfType<ExecutorFailedEvent>().Should().BeEmpty(
$"[{scenarioName}] no executor failures should be raised when responding to the resumed approval request");
}
postApprovalCompletionAssertion?.Invoke(postResumeEvents);
}
/// <summary>
/// Bundles a <see cref="ChatClientAgent"/> + counting <see cref="ApprovalRequiredAIFunction"/>
/// + <see cref="MockChatClient"/> in a single reusable harness so the four end-to-end repro
/// variants (#7, A2, A3, A4) can share identical setup.
/// </summary>
private sealed class ReproHarness
{
public const string ToolName = "GetWeather";
public const string ToolResultText = "Sunny, 22°C";
public const string ToolCallId = "call-1";
public const string FinalAssistantText = "The weather in Amsterdam is sunny and 22°C.";
private int _invocationCount;
private int _chatCallIndex;
public int InvocationCount => Volatile.Read(ref this._invocationCount);
public int ChatCallCount => Volatile.Read(ref this._chatCallIndex);
public ChatClientAgent Agent { get; }
public ReproHarness()
{
AIFunction underlyingTool = AIFunctionFactory.Create(
([Description("City to look up")] string city) =>
{
Interlocked.Increment(ref this._invocationCount);
return ToolResultText;
},
name: ToolName,
description: "Gets the weather for the given city");
ApprovalRequiredAIFunction approvalTool = new(underlyingTool);
MockChatClient mockChatClient = new((messages, options) =>
{
int index = Interlocked.Increment(ref this._chatCallIndex) - 1;
return index switch
{
0 => new ChatResponse(new ChatMessage(ChatRole.Assistant,
[new FunctionCallContent(
callId: ToolCallId,
name: ToolName,
arguments: new Dictionary<string, object?> { ["city"] = "Amsterdam" })])),
_ => new ChatResponse(new ChatMessage(ChatRole.Assistant, FinalAssistantText)),
};
});
this.Agent = new ChatClientAgent(
mockChatClient,
instructions: "You are a weather agent.",
name: "WeatherAgent",
tools: [approvalTool]);
}
}
/// <summary>
/// Wraps a <see cref="JsonCheckpointStore"/> and forces every <see cref="JsonElement"/> to
/// round-trip through <c>GetRawText()</c> + <see cref="JsonDocument.Parse(string,JsonDocumentOptions)"/>
/// during commit and again during retrieve. This emulates a Dapper-backed SQL store where
/// the <see cref="JsonElement"/> is materialized to a string for the <c>nvarchar(max)</c>
/// column on the way in and re-parsed on the way back out. Used by the A3 test only;
/// the double parse-and-clone is intentionally extra work and not suitable for production use.
/// </summary>
private sealed class StringRoundTripJsonStore(JsonCheckpointStore inner) : JsonCheckpointStore
{
public override async ValueTask<CheckpointInfo> CreateCheckpointAsync(string sessionId, JsonElement value, CheckpointInfo? parent = null)
{
JsonElement roundTripped = RoundTrip(value);
return await inner.CreateCheckpointAsync(sessionId, roundTripped, parent).ConfigureAwait(false);
}
public override async ValueTask<JsonElement> RetrieveCheckpointAsync(string sessionId, CheckpointInfo key)
{
JsonElement raw = await inner.RetrieveCheckpointAsync(sessionId, key).ConfigureAwait(false);
return RoundTrip(raw);
}
public override ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(string sessionId, CheckpointInfo? withParent = null)
=> inner.RetrieveIndexAsync(sessionId, withParent);
private static JsonElement RoundTrip(JsonElement element)
{
string raw = element.GetRawText();
using JsonDocument doc = JsonDocument.Parse(raw);
return doc.RootElement.Clone();
}
}
}
+1 -19
View File
@@ -7,23 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.5.0] - 2026-05-19
### Added
- **agent-framework-core**, **agent-framework-foundry**, **agent-framework-openai**: Record actual served model from Azure OpenAI ([#5910](https://github.com/microsoft/agent-framework/pull/5910))
- **samples**: New Foundry Hosted Agents samples for RAG, Skills, and Memory ([#5822](https://github.com/microsoft/agent-framework/pull/5822))
### Changed
- **agent-framework-core**, **agent-framework-azurefunctions**, **agent-framework-devui**, **agent-framework-foundry**, **agent-framework-orchestrations**: Improve handling of intermediate outputs for workflows and orchestrations ([#5623](https://github.com/microsoft/agent-framework/pull/5623))
- **agent-framework-durabletask**: Pin `durabletask` and `durabletask-azuremanaged` floors to `>=1.4.0` and exclude upstream `durabletask` 1.4.1, 1.4.2, and 1.4.3 from the supported version range.
- **agent-framework-orchestrations**: Bumped package to release candidate stage.
### Fixed
- **agent-framework-core**: Parse YAML block scalars in SKILL.md frontmatter ([#5863](https://github.com/microsoft/agent-framework/pull/5863))
- **agent-framework-github-copilot**: Include tools added by `ContextProvider.before_run` in session creation ([#5780](https://github.com/microsoft/agent-framework/pull/5780))
- **agent-framework-hyperlight**: Skip symlinks when staging sandbox input ([#5919](https://github.com/microsoft/agent-framework/pull/5919))
- **agent-framework-purview**: Remove duplicate pop in `InMemoryCacheProvider.remove` ([#5795](https://github.com/microsoft/agent-framework/pull/5795))
## [1.4.0] - 2026-05-14
### Added
@@ -1088,8 +1071,7 @@ Release candidate for **agent-framework-core** and **agent-framework-azure-ai**
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.5.0...HEAD
[1.5.0]: https://github.com/microsoft/agent-framework/compare/python-1.4.0...python-1.5.0
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.4.0...HEAD
[1.4.0]: https://github.com/microsoft/agent-framework/compare/python-1.3.0...python-1.4.0
[1.3.0]: https://github.com/microsoft/agent-framework/compare/python-1.2.2...python-1.3.0
[1.2.2]: https://github.com/microsoft/agent-framework/compare/python-1.2.1...python-1.2.2
+2 -2
View File
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"a2a-sdk>=1.0.0,<2",
]
+2 -2
View File
@@ -22,10 +22,10 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"ag-ui-protocol>=0.1.16,<0.2",
"fastapi>=0.115.0,<0.133.1",
"uvicorn[standard]>=0.30.0,<1"
"uvicorn[standard]>=0.30.0,<0.42.0"
]
[project.optional-dependencies]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"anthropic>=0.80.0,<0.80.1",
]
@@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"azure-search-documents>=11.7.0b2,<11.7.0b3",
]
@@ -4,7 +4,7 @@ description = "Azure Content Understanding integration for Microsoft Agent Frame
authors = [{ name = "Microsoft", email = "af-support@microsoft.com" }]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0a260519"
version = "1.0.0a260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,8 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-foundry>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"agent-framework-foundry>=1.4.0,<2",
"azure-ai-contentunderstanding>=1.0.1,<1.1",
"aiohttp>=3.9,<4",
"filetype>=1.2,<2",
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Azure Cosmos DB history provider integration for Microsoft Agent
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"azure-cosmos>=4.3.0,<5",
]
@@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,8 +22,8 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-durabletask>=1.0.0b260519,<2",
"agent-framework-core>=1.4.0,<2",
"agent-framework-durabletask",
"azure-functions>=1.24.0,<2",
"azure-functions-durable>=1.3.1,<2",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"boto3>=1.35.0,<2.0.0",
"botocore>=1.35.0,<2.0.0",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"openai-chatkit>=1.4.1,<2.0.0",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Claude Agent SDK integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"claude-agent-sdk>=0.1.36,<0.1.49",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"microsoft-agents-copilotstudio-client>=0.3.1,<0.3.2",
]
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.5.0"
version = "1.4.0"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"httpx>=0.27,<1",
"powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
"pyyaml>=6.0,<7.0",
+4 -4
View File
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,18 +23,18 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"openai>=1.99.0,<3",
"opentelemetry-sdk>=1.39.0,<2",
"fastapi>=0.115.0,<0.133.1",
"uvicorn[standard]>=0.30.0,<1"
"uvicorn[standard]>=0.30.0,<0.42.0"
]
[project.optional-dependencies]
dev = [
"pytest==9.0.3",
"watchdog==6.0.0",
"agent-framework-orchestrations==1.0.0rc1",
"agent-framework-orchestrations==1.0.0b260402",
]
all = [
"pytest==9.0.3",
+4 -4
View File
@@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,9 +22,9 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"durabletask>=1.4.0,!=1.4.1,!=1.4.2,!=1.4.3,<2",
"durabletask-azuremanaged>=1.4.0,<2",
"agent-framework-core>=1.4.0,<2",
"durabletask>=1.3.0,<2",
"durabletask-azuremanaged>=1.3.0,<2",
"python-dateutil>=2.8.0,<3",
]
+3 -3
View File
@@ -4,7 +4,7 @@ description = "Microsoft Foundry integrations for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.5.0"
version = "1.4.0"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,8 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-openai>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"agent-framework-openai>=1.4.0,<2",
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
"azure-ai-projects>=2.1.0,<3.0",
]
@@ -4,7 +4,7 @@ description = "Foundry Hosting integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0a260519"
version = "1.0.0a260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"azure-ai-agentserver-core>=2.0.0b3,<3",
"azure-ai-agentserver-responses>=1.0.0b5,<2",
"azure-ai-agentserver-invocations>=1.0.0b3,<2",
+3 -3
View File
@@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,8 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-openai>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"agent-framework-openai>=1.4.0,<2",
"foundry-local-sdk>=0.5.1,<0.5.2",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Google Gemini integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0a260519"
version = "1.0.0a260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -24,7 +24,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2.0",
"agent-framework-core>=1.4.0,<2.0",
"google-genai>=1.65.0,<2.0.0",
]
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Hyperlight CodeAct integrations for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"hyperlight-sandbox>=0.4.0,<0.5",
"hyperlight-sandbox-backend-wasm>=0.4.0,<0.5 ; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'",
"hyperlight-sandbox-python-guest>=0.4.0,<0.5",
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
]
[project.optional-dependencies]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"mem0ai>=1.0.0,<2",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"ollama>=0.5.3,<0.5.4",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "OpenAI integrations for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.5.0"
version = "1.4.0"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"openai>=1.99.0,<3",
]
@@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0rc1"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
]
[tool.uv]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -24,7 +24,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"azure-core>=1.30.0,<2",
"httpx>=0.27.0,<0.29",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260519"
version = "1.0.0b260514"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.5.0,<2",
"agent-framework-core>=1.4.0,<2",
"redis>=6.4.0,<7.2.1",
"redisvl>=0.11.0,<0.16",
"numpy>=2.2.6,<3"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.5.0"
version = "1.4.0"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core[all]==1.5.0",
"agent-framework-core[all]==1.4.0",
]
[dependency-groups]
+1397 -1513
View File
File diff suppressed because it is too large Load Diff