mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
scope MCP threadId to the current agent
This commit is contained in:
@@ -79,6 +79,15 @@ internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient)
|
||||
RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames);
|
||||
AgentSessionId sessionId = durableSession.SessionId;
|
||||
|
||||
// The session must belong to this agent.
|
||||
if (!string.Equals(sessionId.Name, this.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The provided session belongs to agent '{sessionId.Name}' but was passed to agent '{this.Name}'. " +
|
||||
"Sessions cannot be reused across agents.",
|
||||
paramName: nameof(session));
|
||||
}
|
||||
|
||||
AgentRunHandle agentRunHandle = await this._agentClient.RunAgentAsync(sessionId, request, cancellationToken);
|
||||
|
||||
if (isFireAndForget)
|
||||
|
||||
@@ -363,9 +363,10 @@ internal static class BuiltInFunctions
|
||||
|
||||
string agentName = context.Name;
|
||||
|
||||
// Derive session id: try to parse provided threadId, otherwise create a new one.
|
||||
// Bind the caller-supplied threadId as a session key under the current agent name,
|
||||
// mirroring the behavior of RunAgentHttpAsync.
|
||||
AgentSessionId sessionId = context.Arguments.TryGetValue("threadId", out object? threadObj) && threadObj is string threadId && !string.IsNullOrWhiteSpace(threadId)
|
||||
? AgentSessionId.Parse(threadId)
|
||||
? new AgentSessionId(agentName, threadId)
|
||||
: new AgentSessionId(agentName, functionContext.InvocationId);
|
||||
|
||||
AIAgent agentProxy = client.AsDurableAgentProxy(functionContext, agentName);
|
||||
|
||||
@@ -53,4 +53,16 @@ public sealed class AgentSessionIdTests
|
||||
AgentSessionId sessionId = entityId;
|
||||
});
|
||||
}
|
||||
|
||||
// Ensures the 2-arg constructor treats the key as opaque and never re-interprets
|
||||
// it as a serialized session id, so the resulting Name always comes from the first
|
||||
// argument regardless of the key's shape.
|
||||
[Fact]
|
||||
public void ConstructorTreatsKeyAsOpaqueValue()
|
||||
{
|
||||
AgentSessionId sessionId = new("agentA", "@dafx-agentB@some-key");
|
||||
|
||||
Assert.Equal("agentA", sessionId.Name);
|
||||
Assert.Equal("@dafx-agentB@some-key", sessionId.Key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.DurableTask.UnitTests;
|
||||
|
||||
public sealed class DurableAIAgentProxyTests
|
||||
{
|
||||
// Verifies the proxy rejects a session whose agent name differs from its own,
|
||||
// and that the durable client is never called when this happens.
|
||||
[Fact]
|
||||
public async Task RunAsync_ThrowsWhenSessionBelongsToDifferentAgent()
|
||||
{
|
||||
StubDurableAgentClient client = new();
|
||||
DurableAIAgentProxy proxy = new("agentA", client);
|
||||
DurableAgentSession session = new(new AgentSessionId("agentB", "shared-key"));
|
||||
|
||||
ArgumentException ex = await Assert.ThrowsAsync<ArgumentException>(() =>
|
||||
proxy.RunAsync(new ChatMessage(ChatRole.User, "hello"), session));
|
||||
|
||||
Assert.Equal("session", ex.ParamName);
|
||||
Assert.Contains("agentB", ex.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("agentA", ex.Message, StringComparison.Ordinal);
|
||||
Assert.Equal(0, client.CallCount);
|
||||
}
|
||||
|
||||
// Control test: when the session's agent name matches the proxy's name,
|
||||
// the request is forwarded to the durable client.
|
||||
[Fact]
|
||||
public async Task RunAsync_AllowsSessionWhenAgentNameMatches()
|
||||
{
|
||||
AgentSessionId sessionId = new("agentA", "shared-key");
|
||||
InvalidOperationException sentinel = new("reached the client");
|
||||
StubDurableAgentClient client = new() { Throw = sentinel };
|
||||
DurableAIAgentProxy proxy = new("agentA", client);
|
||||
DurableAgentSession session = new(sessionId);
|
||||
|
||||
// Reaching the durable client (and therefore propagating the sentinel) proves the
|
||||
// name-matching guard accepted this session.
|
||||
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
proxy.RunAsync(new ChatMessage(ChatRole.User, "hello"), session));
|
||||
|
||||
Assert.Same(sentinel, ex);
|
||||
Assert.Equal(1, client.CallCount);
|
||||
Assert.Equal(sessionId, client.LastSessionId);
|
||||
}
|
||||
|
||||
// Ensures the agent-name comparison is case-insensitive, so casing differences
|
||||
// are neither a false-positive rejection nor a bypass.
|
||||
[Fact]
|
||||
public async Task RunAsync_AgentNameComparisonIsCaseInsensitive()
|
||||
{
|
||||
AgentSessionId sessionId = new("AGENTA", "shared-key");
|
||||
InvalidOperationException sentinel = new("reached the client");
|
||||
StubDurableAgentClient client = new() { Throw = sentinel };
|
||||
DurableAIAgentProxy proxy = new("agentA", client);
|
||||
DurableAgentSession session = new(sessionId);
|
||||
|
||||
InvalidOperationException ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
proxy.RunAsync(new ChatMessage(ChatRole.User, "hello"), session));
|
||||
|
||||
Assert.Same(sentinel, ex);
|
||||
Assert.Equal(1, client.CallCount);
|
||||
}
|
||||
|
||||
private sealed class StubDurableAgentClient : IDurableAgentClient
|
||||
{
|
||||
public int CallCount { get; private set; }
|
||||
public AgentSessionId LastSessionId { get; private set; }
|
||||
public Exception? Throw { get; set; }
|
||||
|
||||
public Task<AgentRunHandle> RunAgentAsync(
|
||||
AgentSessionId sessionId,
|
||||
RunRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
this.CallCount++;
|
||||
this.LastSessionId = sessionId;
|
||||
if (this.Throw is not null)
|
||||
{
|
||||
return Task.FromException<AgentRunHandle>(this.Throw);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Test did not configure a response.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user