Skip to content

Python: .Net: Magentic orchestration to return the last agent message when limits reached #12839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions dotnet/src/Agents/Magentic/MagenticManagerActor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,16 @@ private async ValueTask ManageAsync(CancellationToken cancellationToken)

if (this._invocationCount >= this._manager.MaximumInvocationCount)
{
await this.PublishMessageAsync("Maximum number of invocations reached.".AsResultMessage(), this._orchestrationType, cancellationToken).ConfigureAwait(false);
this.Logger.LogMagenticManagerTaskFailed(this.Context.Topic);
try
{
var partialResult = this._chat.Last((message) => message.Role == AuthorRole.Assistant);
await this.PublishMessageAsync(partialResult.AsResultMessage(), this._orchestrationType, cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
await this.PublishMessageAsync("I've reaches the maximum number of invocations. No partial result available.".AsResultMessage(), this._orchestrationType, cancellationToken).ConfigureAwait(false);
}
break;
}

Expand All @@ -157,7 +166,15 @@ private async ValueTask ManageAsync(CancellationToken cancellationToken)
if (this._retryCount >= this._manager.MaximumResetCount)
{
this.Logger.LogMagenticManagerTaskFailed(this.Context.Topic);
await this.PublishMessageAsync("I've experienced multiple failures and am unable to continue.".AsResultMessage(), this._orchestrationType, cancellationToken).ConfigureAwait(false);
try
{
var partialResult = this._chat.Last((message) => message.Role == AuthorRole.Assistant);
await this.PublishMessageAsync(partialResult.AsResultMessage(), this._orchestrationType, cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
await this.PublishMessageAsync("I've experienced multiple failures and am unable to continue. No partial result available.".AsResultMessage(), this._orchestrationType, cancellationToken).ConfigureAwait(false);
}
break;
}

Expand Down
236 changes: 236 additions & 0 deletions dotnet/src/Agents/UnitTests/Magentic/MagenticOrchestrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,216 @@ public async Task MagenticOrchestrationWithMultipleAgentsAsync()
Assert.Equal(0, mockAgent3.InvokeCount);
}

[Fact]
public async Task MagenticOrchestrationMaxInvocationCountReached_WithoutPartialResultAsync()
{
// Arrange
await using InProcessRuntime runtime = new();

MockAgent mockAgent1 = CreateMockAgent(1, "abc");
MockAgent mockAgent2 = CreateMockAgent(2, "xyz");
MockAgent mockAgent3 = CreateMockAgent(3, "lmn");

string jsonStatus =
$$"""
{
"Name": "{{mockAgent1.Name}}",
"Instruction":"Proceed",
"Reason":"TestReason",
"IsTaskComplete": {
"Result": false,
"Reason": "Test"
},
"IsTaskProgressing": {
"Result": true,
"Reason": "Test"
},
"IsTaskInLoop": {
"Result": false,
"Reason": "Test"
}
}
""";
Mock<IChatCompletionService> chatServiceMock = CreateMockChatCompletionService(jsonStatus);

FakePromptExecutionSettings settings = new();
StandardMagenticManager manager = new(chatServiceMock.Object, settings)
{
MaximumInvocationCount = 1, // Fast failure for testing
};

MagenticOrchestration orchestration = new(manager, [mockAgent1, mockAgent2, mockAgent3]);

// Act
await runtime.StartAsync();

const string InitialInput = "123";
OrchestrationResult<string> result = await orchestration.InvokeAsync(InitialInput, runtime);
string response = await result.GetValueAsync(TimeSpan.FromSeconds(20));

// Assert
Assert.NotNull(response);
Assert.Contains("No partial result available.", response);
}

[Fact]
public async Task MagenticOrchestrationMaxInvocationCountReached_WithPartialResultAsync()
{
// Arrange
await using InProcessRuntime runtime = new();

MockAgent mockAgent1 = CreateMockAgent(1, "abc");
MockAgent mockAgent2 = CreateMockAgent(2, "xyz");
MockAgent mockAgent3 = CreateMockAgent(3, "lmn");

string jsonStatus =
$$"""
{
"Name": "{{mockAgent1.Name}}",
"Instruction":"Proceed",
"Reason":"TestReason",
"IsTaskComplete": {
"Result": false,
"Reason": "Test"
},
"IsTaskProgressing": {
"Result": true,
"Reason": "Test"
},
"IsTaskInLoop": {
"Result": false,
"Reason": "Test"
}
}
""";
Mock<IChatCompletionService> chatServiceMock = CreateMockChatCompletionService(jsonStatus);

FakePromptExecutionSettings settings = new();
StandardMagenticManager manager = new(chatServiceMock.Object, settings)
{
MaximumInvocationCount = 2, // Fast failure for testing but at least one invocation
};

MagenticOrchestration orchestration = new(manager, [mockAgent1, mockAgent2, mockAgent3]);

// Act
await runtime.StartAsync();

const string InitialInput = "123";
OrchestrationResult<string> result = await orchestration.InvokeAsync(InitialInput, runtime);
string response = await result.GetValueAsync(TimeSpan.FromSeconds(20));

// Assert
Assert.NotNull(response);
Assert.Equal("abc", response);
}

[Fact]
public async Task MagenticOrchestrationMaxResetCountReached_WithoutPartialResultAsync()
{
// Arrange
await using InProcessRuntime runtime = new();

MockAgent mockAgent1 = CreateMockAgent(1, "abc");
MockAgent mockAgent2 = CreateMockAgent(2, "xyz");
MockAgent mockAgent3 = CreateMockAgent(3, "lmn");

string jsonStatus =
$$"""
{
"Name": "{{mockAgent1.Name}}",
"Instruction":"Proceed",
"Reason":"TestReason",
"IsTaskComplete": {
"Result": false,
"Reason": "Test"
},
"IsTaskProgressing": {
"Result": false,
"Reason": "Test"
},
"IsTaskInLoop": {
"Result": true,
"Reason": "Test"
}
}
""";
Mock<IChatCompletionService> chatServiceMock = CreateMockChatCompletionService(jsonStatus);

FakePromptExecutionSettings settings = new();
StandardMagenticManager manager = new(chatServiceMock.Object, settings)
{
MaximumResetCount = 1, // Fast failure for testing
MaximumStallCount = 0, // No stalls allowed
};

MagenticOrchestration orchestration = new(manager, [mockAgent1, mockAgent2, mockAgent3]);

// Act
await runtime.StartAsync();

const string InitialInput = "123";
OrchestrationResult<string> result = await orchestration.InvokeAsync(InitialInput, runtime);
string response = await result.GetValueAsync(TimeSpan.FromSeconds(20));

// Assert
Assert.NotNull(response);
Assert.Contains("No partial result available.", response);
}

[Fact]
public async Task MagenticOrchestrationMaxResetCountReached_WithPartialResultAsync()
{
// Arrange
await using InProcessRuntime runtime = new();

MockAgent mockAgent1 = CreateMockAgent(1, "abc");
MockAgent mockAgent2 = CreateMockAgent(2, "xyz");
MockAgent mockAgent3 = CreateMockAgent(3, "lmn");

string jsonStatus =
$$"""
{
"Name": "{{mockAgent1.Name}}",
"Instruction":"Proceed",
"Reason":"TestReason",
"IsTaskComplete": {
"Result": false,
"Reason": "Test"
},
"IsTaskProgressing": {
"Result": false,
"Reason": "Test"
},
"IsTaskInLoop": {
"Result": true,
"Reason": "Test"
}
}
""";
Mock<IChatCompletionService> chatServiceMock = CreateMockChatCompletionService(jsonStatus);

FakePromptExecutionSettings settings = new();
StandardMagenticManager manager = new(chatServiceMock.Object, settings)
{
MaximumResetCount = 1, // Fast failure for testing but at least one response
MaximumStallCount = 2, // Allow some stalls for at least one response
};

MagenticOrchestration orchestration = new(manager, [mockAgent1, mockAgent2, mockAgent3]);

// Act
await runtime.StartAsync();

const string InitialInput = "123";
OrchestrationResult<string> result = await orchestration.InvokeAsync(InitialInput, runtime);
string response = await result.GetValueAsync(TimeSpan.FromSeconds(20));

// Assert
Assert.NotNull(response);
Assert.Contains("abc", response);
}

private async Task<string> ExecuteOrchestrationAsync(InProcessRuntime runtime, string answer, params Agent[] mockAgents)
{
// Act
Expand All @@ -81,6 +291,7 @@ private static MockAgent CreateMockAgent(int index, string response)
{
return new()
{
Name = $"MockAgent{index}",
Description = $"test {index}",
Response = [new(AuthorRole.Assistant, response)]
};
Expand Down Expand Up @@ -130,4 +341,29 @@ MagenticProgressLedger CreateLedger(bool isTaskComplete, string name)
}
}
}

private static Mock<IChatCompletionService> CreateMockChatCompletionService(string response)
{
Mock<IChatCompletionService> chatServiceMock = new(MockBehavior.Strict);

chatServiceMock.Setup(
(service) => service.GetChatMessageContentsAsync(
It.IsAny<ChatHistory>(),
It.IsAny<PromptExecutionSettings>(),
null,
It.IsAny<CancellationToken>()))
.ReturnsAsync([new ChatMessageContent(AuthorRole.Assistant, response)]);

return chatServiceMock;
}

private sealed class FakePromptExecutionSettings : PromptExecutionSettings
{
public override PromptExecutionSettings Clone()
{
return this;
}

public object? ResponseFormat { get; set; }
}
}
2 changes: 1 addition & 1 deletion python/semantic_kernel/agents/orchestration/magentic.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ async def _check_within_limits(self) -> bool:

if hit_round_limit or hit_reset_limit:
limit_type = "round" if hit_round_limit else "reset"
logger.debug(f"Max {limit_type} count reached.")
logger.error(f"Max {limit_type} count reached.")

# Retrieve the latest assistant content produced so far
partial_result = next(
Expand Down
Loading