mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
035b5b86e7 | ||
|
|
1011a415c0 | ||
|
|
e53e87b39e | ||
|
|
8d503c03aa | ||
|
|
e28682a853 |
@@ -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 2–4) 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.
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
+17
-47
@@ -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; } = [];
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
-29
@@ -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 |
|
||||
-36
@@ -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!")
|
||||
```
|
||||
-97
@@ -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
|
||||
```
|
||||
+838
@@ -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<ToolApprovalRequestContent>()</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<ToolApprovalRequestContent, ToolApprovalResponseContent></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<ToolApprovalRequestContent>().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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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,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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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'",
|
||||
]
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
Generated
+1397
-1513
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user