mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Require TODO finish reason and rename SubAgents to BackgroundAgents (#5902)
* Require TODO finish reason and rename SubAgents to BackgroundAgents * Address PR comments
This commit is contained in:
@@ -74,9 +74,11 @@ internal static partial class AgentJsonUtilities
|
||||
[JsonSerializable(typeof(TodoState))]
|
||||
[JsonSerializable(typeof(TodoItem))]
|
||||
[JsonSerializable(typeof(TodoItemInput))]
|
||||
[JsonSerializable(typeof(TodoCompleteInput))]
|
||||
[JsonSerializable(typeof(List<int>), TypeInfoPropertyName = "IntList")]
|
||||
[JsonSerializable(typeof(List<TodoItem>), TypeInfoPropertyName = "TodoItemList")]
|
||||
[JsonSerializable(typeof(List<TodoItemInput>), TypeInfoPropertyName = "TodoItemInputList")]
|
||||
[JsonSerializable(typeof(List<TodoCompleteInput>), TypeInfoPropertyName = "TodoCompleteInputList")]
|
||||
|
||||
// AgentModeProvider types
|
||||
[JsonSerializable(typeof(AgentModeState))]
|
||||
@@ -95,12 +97,12 @@ internal static partial class AgentJsonUtilities
|
||||
[JsonSerializable(typeof(FileListEntry))]
|
||||
[JsonSerializable(typeof(List<FileListEntry>), TypeInfoPropertyName = "FileListEntryList")]
|
||||
|
||||
// SubAgentsProvider types
|
||||
[JsonSerializable(typeof(SubAgentState))]
|
||||
[JsonSerializable(typeof(SubAgentRuntimeState))]
|
||||
[JsonSerializable(typeof(SubTaskInfo))]
|
||||
[JsonSerializable(typeof(SubTaskStatus))]
|
||||
[JsonSerializable(typeof(List<SubTaskInfo>), TypeInfoPropertyName = "SubTaskInfoList")]
|
||||
// BackgroundAgentsProvider types
|
||||
[JsonSerializable(typeof(BackgroundAgentState))]
|
||||
[JsonSerializable(typeof(BackgroundAgentRuntimeState))]
|
||||
[JsonSerializable(typeof(BackgroundTaskInfo))]
|
||||
[JsonSerializable(typeof(BackgroundTaskStatus))]
|
||||
[JsonSerializable(typeof(List<BackgroundTaskInfo>), TypeInfoPropertyName = "BackgroundTaskInfoList")]
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
internal sealed partial class JsonContext : JsonSerializerContext;
|
||||
|
||||
+5
-5
@@ -7,15 +7,15 @@ using System.Threading.Tasks;
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Holds non-serializable runtime references for in-flight sub-tasks within a single parent session.
|
||||
/// Holds non-serializable runtime references for in-flight background tasks within a single parent session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Properties are marked with <see cref="JsonIgnoreAttribute"/> because <see cref="Task{TResult}"/>
|
||||
/// and <see cref="AgentSession"/> are not JSON-serializable. After deserialization (e.g., after a restart),
|
||||
/// a fresh empty instance is created and any previously-running tasks are marked as
|
||||
/// <see cref="SubTaskStatus.Lost"/> by <see cref="SubAgentsProvider"/>.
|
||||
/// <see cref="BackgroundTaskStatus.Lost"/> by <see cref="BackgroundAgentsProvider"/>.
|
||||
/// </remarks>
|
||||
internal sealed class SubAgentRuntimeState
|
||||
internal sealed class BackgroundAgentRuntimeState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the mapping of task IDs to their in-flight <see cref="Task{AgentResponse}"/> instances.
|
||||
@@ -24,9 +24,9 @@ internal sealed class SubAgentRuntimeState
|
||||
public Dictionary<int, Task<AgentResponse>> InFlightTasks { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mapping of task IDs to their sub-agent <see cref="AgentSession"/> instances,
|
||||
/// Gets the mapping of task IDs to their background agent <see cref="AgentSession"/> instances,
|
||||
/// needed for <c>ContinueTask</c>.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Dictionary<int, AgentSession> SubTaskSessions { get; } = [];
|
||||
public Dictionary<int, AgentSession> BackgroundTaskSessions { get; } = [];
|
||||
}
|
||||
+5
-5
@@ -8,21 +8,21 @@ using Microsoft.Shared.DiagnosticIds;
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the serializable state of sub-tasks managed by the <see cref="SubAgentsProvider"/>,
|
||||
/// Represents the serializable state of background tasks managed by the <see cref="BackgroundAgentsProvider"/>,
|
||||
/// stored in the session's <see cref="AgentSessionStateBag"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
internal sealed class SubAgentState
|
||||
internal sealed class BackgroundAgentState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the next ID to assign to a new sub-task.
|
||||
/// Gets or sets the next ID to assign to a new background task.
|
||||
/// </summary>
|
||||
[JsonPropertyName("nextTaskId")]
|
||||
public int NextTaskId { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of sub-task metadata entries.
|
||||
/// Gets the list of background task metadata entries.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tasks")]
|
||||
public List<SubTaskInfo> Tasks { get; set; } = [];
|
||||
public List<BackgroundTaskInfo> Tasks { get; set; } = [];
|
||||
}
|
||||
+81
-81
@@ -15,56 +15,56 @@ using Microsoft.Shared.Diagnostics;
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="AIContextProvider"/> that enables an agent to delegate work to sub-agents asynchronously.
|
||||
/// An <see cref="AIContextProvider"/> that enables an agent to delegate work to background agents asynchronously.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="SubAgentsProvider"/> allows a parent agent to start sub-tasks on child agents,
|
||||
/// wait for their completion, and retrieve results. Each sub-task runs in its own session and
|
||||
/// The <see cref="BackgroundAgentsProvider"/> allows a parent agent to start background tasks on child agents,
|
||||
/// wait for their completion, and retrieve results. Each background task runs in its own session and
|
||||
/// executes concurrently.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This provider exposes the following tools to the agent:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>SubAgents_StartTask</c> — Start a sub-task on a named agent with text input. Returns the task ID.</description></item>
|
||||
/// <item><description><c>SubAgents_WaitForFirstCompletion</c> — Block until the first of the specified tasks completes. Returns the completed task's ID.</description></item>
|
||||
/// <item><description><c>SubAgents_GetTaskResults</c> — Retrieve the text output of a completed sub-task.</description></item>
|
||||
/// <item><description><c>SubAgents_GetAllTasks</c> — List all sub-tasks with their IDs, statuses, descriptions, and agent names.</description></item>
|
||||
/// <item><description><c>SubAgents_ContinueTask</c> — Send follow-up input to a completed sub-task's session to resume work.</description></item>
|
||||
/// <item><description><c>SubAgents_ClearCompletedTask</c> — Remove a completed sub-task and release its session to free memory.</description></item>
|
||||
/// <item><description><c>BackgroundAgents_StartTask</c> — Start a background task on a named agent with text input. Returns the task ID.</description></item>
|
||||
/// <item><description><c>BackgroundAgents_WaitForFirstCompletion</c> — Block until the first of the specified tasks completes. Returns the completed task's ID.</description></item>
|
||||
/// <item><description><c>BackgroundAgents_GetTaskResults</c> — Retrieve the text output of a completed background task.</description></item>
|
||||
/// <item><description><c>BackgroundAgents_GetAllTasks</c> — List all background tasks with their IDs, statuses, descriptions, and agent names.</description></item>
|
||||
/// <item><description><c>BackgroundAgents_ContinueTask</c> — Send follow-up input to a completed background task's session to resume work.</description></item>
|
||||
/// <item><description><c>BackgroundAgents_ClearCompletedTask</c> — Remove a completed background task and release its session to free memory.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class SubAgentsProvider : AIContextProvider
|
||||
public sealed class BackgroundAgentsProvider : AIContextProvider
|
||||
{
|
||||
private const string DefaultInstructions =
|
||||
"""
|
||||
## SubAgents
|
||||
You have access to sub-agents that can perform work on your behalf.
|
||||
## BackgroundAgents
|
||||
You have access to background agents that can perform work on your behalf.
|
||||
|
||||
- Use the `SubAgents_*` list of tools to start tasks on sub agents and check their results.
|
||||
- Creating a sub task does not block, and sub-tasks run concurrently.
|
||||
- Use the `BackgroundAgents_*` list of tools to start tasks on background agents and check their results.
|
||||
- Creating a background task does not block, and background tasks run concurrently.
|
||||
- Important: Always wait for outstanding tasks to finish before you finish processing.
|
||||
- Important: After retrieving results from a completed task, clear it with SubAgents_ClearCompletedTask to free memory, unless you plan to continue it with SubAgents_ContinueTask.
|
||||
- Important: After retrieving results from a completed task, clear it with BackgroundAgents_ClearCompletedTask to free memory, unless you plan to continue it with BackgroundAgents_ContinueTask.
|
||||
|
||||
{sub_agents}
|
||||
{background_agents}
|
||||
""";
|
||||
|
||||
private readonly Dictionary<string, AIAgent> _agents;
|
||||
private readonly ProviderSessionState<SubAgentState> _sessionState;
|
||||
private readonly ProviderSessionState<SubAgentRuntimeState> _runtimeSessionState;
|
||||
private readonly ProviderSessionState<BackgroundAgentState> _sessionState;
|
||||
private readonly ProviderSessionState<BackgroundAgentRuntimeState> _runtimeSessionState;
|
||||
private readonly string _instructions;
|
||||
private IReadOnlyList<string>? _stateKeys;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubAgentsProvider"/> class.
|
||||
/// Initializes a new instance of the <see cref="BackgroundAgentsProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="agents">The collection of sub-agents available for delegation.</param>
|
||||
/// <param name="agents">The collection of background agents available for delegation.</param>
|
||||
/// <param name="options">Optional settings controlling the provider behavior.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="agents"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException">An agent has a null or empty name, or agent names are not unique.</exception>
|
||||
public SubAgentsProvider(IEnumerable<AIAgent> agents, SubAgentsProviderOptions? options = null)
|
||||
public BackgroundAgentsProvider(IEnumerable<AIAgent> agents, BackgroundAgentsProviderOptions? options = null)
|
||||
{
|
||||
_ = Throw.IfNull(agents);
|
||||
|
||||
@@ -74,15 +74,15 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
string agentListText = options?.AgentListBuilder is not null
|
||||
? options.AgentListBuilder(this._agents)
|
||||
: BuildDefaultAgentListText(this._agents);
|
||||
this._instructions = baseInstructions.Replace("{sub_agents}", agentListText);
|
||||
this._instructions = baseInstructions.Replace("{background_agents}", agentListText);
|
||||
|
||||
this._sessionState = new ProviderSessionState<SubAgentState>(
|
||||
_ => new SubAgentState(),
|
||||
this._sessionState = new ProviderSessionState<BackgroundAgentState>(
|
||||
_ => new BackgroundAgentState(),
|
||||
this.GetType().Name,
|
||||
AgentJsonUtilities.DefaultOptions);
|
||||
|
||||
this._runtimeSessionState = new ProviderSessionState<SubAgentRuntimeState>(
|
||||
_ => new SubAgentRuntimeState(),
|
||||
this._runtimeSessionState = new ProviderSessionState<BackgroundAgentRuntimeState>(
|
||||
_ => new BackgroundAgentRuntimeState(),
|
||||
this.GetType().Name + "_Runtime",
|
||||
AgentJsonUtilities.DefaultOptions);
|
||||
}
|
||||
@@ -93,8 +93,8 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
SubAgentState state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
SubAgentRuntimeState runtimeState = this._runtimeSessionState.GetOrInitializeState(context.Session);
|
||||
BackgroundAgentState state = this._sessionState.GetOrInitializeState(context.Session);
|
||||
BackgroundAgentRuntimeState runtimeState = this._runtimeSessionState.GetOrInitializeState(context.Session);
|
||||
|
||||
return new ValueTask<AIContext>(new AIContext
|
||||
{
|
||||
@@ -113,12 +113,12 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(agent.Name))
|
||||
{
|
||||
throw new ArgumentException("All sub-agents must have a non-empty Name.", nameof(agents));
|
||||
throw new ArgumentException("All background agents must have a non-empty Name.", nameof(agents));
|
||||
}
|
||||
|
||||
if (dict.ContainsKey(agent.Name))
|
||||
{
|
||||
throw new ArgumentException($"Duplicate sub-agent name: '{agent.Name}'. Agent names must be unique (case-insensitive).", nameof(agents));
|
||||
throw new ArgumentException($"Duplicate background agent name: '{agent.Name}'. Agent names must be unique (case-insensitive).", nameof(agents));
|
||||
}
|
||||
|
||||
dict[agent.Name] = agent;
|
||||
@@ -126,19 +126,19 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
|
||||
if (dict.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one sub-agent must be provided.", nameof(agents));
|
||||
throw new ArgumentException("At least one background agent must be provided.", nameof(agents));
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the default text listing available sub-agents and their descriptions.
|
||||
/// Builds the default text listing available background agents and their descriptions.
|
||||
/// </summary>
|
||||
private static string BuildDefaultAgentListText(IReadOnlyDictionary<string, AIAgent> agents)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Available sub-agents:");
|
||||
sb.AppendLine("Available background agents:");
|
||||
foreach (var kvp in agents)
|
||||
{
|
||||
sb.Append("- ").Append(kvp.Key);
|
||||
@@ -156,12 +156,12 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
/// <summary>
|
||||
/// Refreshes the status of in-flight tasks in the given state for the specified session.
|
||||
/// </summary>
|
||||
private void TryRefreshTaskState(SubAgentState state, SubAgentRuntimeState runtimeState, AgentSession? session)
|
||||
private void TryRefreshTaskState(BackgroundAgentState state, BackgroundAgentRuntimeState runtimeState, AgentSession? session)
|
||||
{
|
||||
bool changed = false;
|
||||
foreach (SubTaskInfo task in state.Tasks)
|
||||
foreach (BackgroundTaskInfo task in state.Tasks)
|
||||
{
|
||||
if (task.Status != SubTaskStatus.Running)
|
||||
if (task.Status != BackgroundTaskStatus.Running)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -169,7 +169,7 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
if (!runtimeState.InFlightTasks.TryGetValue(task.Id, out Task<AgentResponse>? inFlight))
|
||||
{
|
||||
// In-flight reference lost (e.g., after restart/deserialization).
|
||||
task.Status = SubTaskStatus.Lost;
|
||||
task.Status = BackgroundTaskStatus.Lost;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
@@ -188,32 +188,32 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes a task by extracting results from the completed Task and updating the SubTaskInfo.
|
||||
/// Finalizes a task by extracting results from the completed Task and updating the BackgroundTaskInfo.
|
||||
/// </summary>
|
||||
private static void FinalizeTask(SubTaskInfo taskInfo, Task<AgentResponse> completedTask, SubAgentRuntimeState runtimeState)
|
||||
private static void FinalizeTask(BackgroundTaskInfo taskInfo, Task<AgentResponse> completedTask, BackgroundAgentRuntimeState runtimeState)
|
||||
{
|
||||
if (completedTask.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
taskInfo.Status = SubTaskStatus.Completed;
|
||||
taskInfo.Status = BackgroundTaskStatus.Completed;
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits — task is already completed
|
||||
taskInfo.ResultText = completedTask.Result.Text;
|
||||
#pragma warning restore VSTHRD002
|
||||
}
|
||||
else if (completedTask.IsFaulted)
|
||||
{
|
||||
taskInfo.Status = SubTaskStatus.Failed;
|
||||
taskInfo.Status = BackgroundTaskStatus.Failed;
|
||||
taskInfo.ErrorText = completedTask.Exception?.InnerException?.Message ?? completedTask.Exception?.Message ?? "Unknown error";
|
||||
}
|
||||
else if (completedTask.IsCanceled)
|
||||
{
|
||||
taskInfo.Status = SubTaskStatus.Failed;
|
||||
taskInfo.Status = BackgroundTaskStatus.Failed;
|
||||
taskInfo.ErrorText = "Task was canceled.";
|
||||
}
|
||||
|
||||
runtimeState.InFlightTasks.Remove(taskInfo.Id);
|
||||
}
|
||||
|
||||
private AITool[] CreateTools(SubAgentState state, SubAgentRuntimeState runtimeState, AgentSession? session)
|
||||
private AITool[] CreateTools(BackgroundAgentState state, BackgroundAgentRuntimeState runtimeState, AgentSession? session)
|
||||
{
|
||||
var serializerOptions = AgentJsonUtilities.DefaultOptions;
|
||||
|
||||
@@ -221,43 +221,43 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
[
|
||||
AIFunctionFactory.Create(
|
||||
async (
|
||||
[Description("The name of the sub agent to delegate the task to.")] string agentName,
|
||||
[Description("The request to pass to the sub agent.")] string input,
|
||||
[Description("The name of the background agent to delegate the task to.")] string agentName,
|
||||
[Description("The request to pass to the background agent.")] string input,
|
||||
[Description("A description of the task used to identify the task later.")] string description) =>
|
||||
{
|
||||
if (!this._agents.TryGetValue(agentName, out AIAgent? agent))
|
||||
{
|
||||
return $"Error: No sub-agent found with name '{agentName}'. Available agents: {string.Join(", ", this._agents.Keys)}";
|
||||
return $"Error: No background agent found with name '{agentName}'. Available agents: {string.Join(", ", this._agents.Keys)}";
|
||||
}
|
||||
|
||||
int taskId = state.NextTaskId++;
|
||||
var taskInfo = new SubTaskInfo
|
||||
var taskInfo = new BackgroundTaskInfo
|
||||
{
|
||||
Id = taskId,
|
||||
AgentName = agentName,
|
||||
Description = description,
|
||||
Status = SubTaskStatus.Running,
|
||||
Status = BackgroundTaskStatus.Running,
|
||||
};
|
||||
state.Tasks.Add(taskInfo);
|
||||
|
||||
// Create a dedicated session for this sub-task so it can be continued later.
|
||||
// Create a dedicated session for this background task so it can be continued later.
|
||||
AgentSession subSession = await agent.CreateSessionAsync().ConfigureAwait(false);
|
||||
|
||||
// Wrap in Task.Run to fork the ExecutionContext. AIAgent.RunAsync is a non-async
|
||||
// method that synchronously sets the static AsyncLocal CurrentRunContext. Without
|
||||
// this isolation, the sub-agent's RunAsync would overwrite the outer (calling)
|
||||
// this isolation, the background agent's RunAsync would overwrite the outer (calling)
|
||||
// agent's CurrentRunContext, corrupting all subsequent tool invocations in the
|
||||
// same FICC batch.
|
||||
runtimeState.InFlightTasks[taskId] = Task.Run(() => agent.RunAsync(input, subSession));
|
||||
runtimeState.SubTaskSessions[taskId] = subSession;
|
||||
runtimeState.BackgroundTaskSessions[taskId] = subSession;
|
||||
|
||||
this._sessionState.SaveState(session, state);
|
||||
return $"Sub-task {taskId} started on agent '{agentName}'.";
|
||||
return $"Background task {taskId} started on agent '{agentName}'.";
|
||||
},
|
||||
new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "SubAgents_StartTask",
|
||||
Description = "Start a sub-task on a named sub-agent. Returns a confirmation message containing the task ID.",
|
||||
Name = "BackgroundAgents_StartTask",
|
||||
Description = "Start a background task on a named background agent. Returns a confirmation message containing the task ID.",
|
||||
SerializerOptions = serializerOptions,
|
||||
}),
|
||||
|
||||
@@ -287,7 +287,7 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
this._sessionState.SaveState(session, state);
|
||||
|
||||
// Check if any of the requested IDs are already complete.
|
||||
SubTaskInfo? alreadyComplete = state.Tasks.FirstOrDefault(t => taskIds.Contains(t.Id) && t.Status != SubTaskStatus.Running);
|
||||
BackgroundTaskInfo? alreadyComplete = state.Tasks.FirstOrDefault(t => taskIds.Contains(t.Id) && t.Status != BackgroundTaskStatus.Running);
|
||||
if (alreadyComplete is not null)
|
||||
{
|
||||
return $"Task {alreadyComplete.Id} is not running; current status: {alreadyComplete.Status}.";
|
||||
@@ -303,7 +303,7 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
var completedEntry = waitableTasks.First(t => t.Task == completedTask);
|
||||
|
||||
// Finalize the completed task.
|
||||
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == completedEntry.Id);
|
||||
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == completedEntry.Id);
|
||||
if (taskInfo is not null)
|
||||
{
|
||||
FinalizeTask(taskInfo, completedEntry.Task, runtimeState);
|
||||
@@ -314,8 +314,8 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
},
|
||||
new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "SubAgents_WaitForFirstCompletion",
|
||||
Description = "Block until the first of the specified sub-tasks completes. Provide one or more task IDs. Returns a status message containing the ID of the task that completed first.",
|
||||
Name = "BackgroundAgents_WaitForFirstCompletion",
|
||||
Description = "Block until the first of the specified background tasks completes. Provide one or more task IDs. Returns a status message containing the ID of the task that completed first.",
|
||||
SerializerOptions = serializerOptions,
|
||||
}),
|
||||
|
||||
@@ -324,7 +324,7 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
{
|
||||
this.TryRefreshTaskState(state, runtimeState, session);
|
||||
|
||||
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
|
||||
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
|
||||
if (taskInfo is null)
|
||||
{
|
||||
return $"Error: No task found with ID {taskId}.";
|
||||
@@ -332,17 +332,17 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
|
||||
return taskInfo.Status switch
|
||||
{
|
||||
SubTaskStatus.Completed => taskInfo.ResultText ?? "(no output)",
|
||||
SubTaskStatus.Failed => $"Task failed: {taskInfo.ErrorText ?? "Unknown error"}",
|
||||
SubTaskStatus.Lost => "Task state was lost (reference unavailable).",
|
||||
SubTaskStatus.Running => $"Task {taskId} is still running.",
|
||||
BackgroundTaskStatus.Completed => taskInfo.ResultText ?? "(no output)",
|
||||
BackgroundTaskStatus.Failed => $"Task failed: {taskInfo.ErrorText ?? "Unknown error"}",
|
||||
BackgroundTaskStatus.Lost => "Task state was lost (reference unavailable).",
|
||||
BackgroundTaskStatus.Running => $"Task {taskId} is still running.",
|
||||
_ => $"Task {taskId} has status: {taskInfo.Status}.",
|
||||
};
|
||||
},
|
||||
new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "SubAgents_GetTaskResults",
|
||||
Description = "Get the text output of a sub-task by its ID. Returns the result text if complete, or status information if still running or failed.",
|
||||
Name = "BackgroundAgents_GetTaskResults",
|
||||
Description = "Get the text output of a background task by its ID. Returns the result text if complete, or status information if still running or failed.",
|
||||
SerializerOptions = serializerOptions,
|
||||
}),
|
||||
|
||||
@@ -358,7 +358,7 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("Tasks:");
|
||||
foreach (SubTaskInfo task in state.Tasks)
|
||||
foreach (BackgroundTaskInfo task in state.Tasks)
|
||||
{
|
||||
sb.Append("- Task ").Append(task.Id).Append(" [").Append(task.Status).Append("] (").Append(task.AgentName).Append("): ").AppendLine(task.Description);
|
||||
}
|
||||
@@ -367,8 +367,8 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
},
|
||||
new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "SubAgents_GetAllTasks",
|
||||
Description = "List all sub-tasks with their IDs, statuses, agent names, and descriptions.",
|
||||
Name = "BackgroundAgents_GetAllTasks",
|
||||
Description = "List all background tasks with their IDs, statuses, agent names, and descriptions.",
|
||||
SerializerOptions = serializerOptions,
|
||||
}),
|
||||
|
||||
@@ -377,18 +377,18 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
{
|
||||
this.TryRefreshTaskState(state, runtimeState, session);
|
||||
|
||||
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
|
||||
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
|
||||
if (taskInfo is null)
|
||||
{
|
||||
return $"Error: No task found with ID {taskId}.";
|
||||
}
|
||||
|
||||
if (taskInfo.Status == SubTaskStatus.Lost)
|
||||
if (taskInfo.Status == BackgroundTaskStatus.Lost)
|
||||
{
|
||||
return $"Error: Task {taskId} cannot be continued because its session was lost (e.g., after a session restore). Start a new task instead.";
|
||||
}
|
||||
|
||||
if (taskInfo.Status == SubTaskStatus.Running)
|
||||
if (taskInfo.Status == BackgroundTaskStatus.Running)
|
||||
{
|
||||
return $"Error: Task {taskId} is still running. Wait for it to complete before continuing.";
|
||||
}
|
||||
@@ -398,17 +398,17 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
return $"Error: Agent '{taskInfo.AgentName}' is no longer available.";
|
||||
}
|
||||
|
||||
if (!runtimeState.SubTaskSessions.TryGetValue(taskId, out AgentSession? subSession))
|
||||
if (!runtimeState.BackgroundTaskSessions.TryGetValue(taskId, out AgentSession? subSession))
|
||||
{
|
||||
return $"Error: Session for task {taskId} is no longer available.";
|
||||
}
|
||||
|
||||
// Reset task state and start a new run on the existing session.
|
||||
taskInfo.Status = SubTaskStatus.Running;
|
||||
taskInfo.Status = BackgroundTaskStatus.Running;
|
||||
taskInfo.ResultText = null;
|
||||
taskInfo.ErrorText = null;
|
||||
|
||||
// Wrap in Task.Run to isolate the ExecutionContext (see StartSubTask comment).
|
||||
// Wrap in Task.Run to isolate the ExecutionContext (see StartBackgroundTask comment).
|
||||
runtimeState.InFlightTasks[taskId] = Task.Run(() => agent.RunAsync(text, subSession));
|
||||
|
||||
this._sessionState.SaveState(session, state);
|
||||
@@ -416,8 +416,8 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
},
|
||||
new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "SubAgents_ContinueTask",
|
||||
Description = "Send follow-up input to a completed or failed sub-task to resume its work. The sub-task's session is preserved, so the agent retains conversational context.",
|
||||
Name = "BackgroundAgents_ContinueTask",
|
||||
Description = "Send follow-up input to a completed or failed background task to resume its work. The background task's session is preserved, so the agent retains conversational context.",
|
||||
SerializerOptions = serializerOptions,
|
||||
}),
|
||||
|
||||
@@ -426,13 +426,13 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
{
|
||||
this.TryRefreshTaskState(state, runtimeState, session);
|
||||
|
||||
SubTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
|
||||
BackgroundTaskInfo? taskInfo = state.Tasks.FirstOrDefault(t => t.Id == taskId);
|
||||
if (taskInfo is null)
|
||||
{
|
||||
return $"Error: No task found with ID {taskId}.";
|
||||
}
|
||||
|
||||
if (taskInfo.Status == SubTaskStatus.Running)
|
||||
if (taskInfo.Status == BackgroundTaskStatus.Running)
|
||||
{
|
||||
return $"Error: Task {taskId} is still running. Wait for it to complete before clearing.";
|
||||
}
|
||||
@@ -442,15 +442,15 @@ public sealed class SubAgentsProvider : AIContextProvider
|
||||
|
||||
// Clean up runtime references.
|
||||
runtimeState.InFlightTasks.Remove(taskId);
|
||||
runtimeState.SubTaskSessions.Remove(taskId);
|
||||
runtimeState.BackgroundTaskSessions.Remove(taskId);
|
||||
|
||||
this._sessionState.SaveState(session, state);
|
||||
return $"Task {taskId} cleared.";
|
||||
},
|
||||
new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "SubAgents_ClearCompletedTask",
|
||||
Description = "Remove a completed or failed sub-task and release its session to free memory. Use this after retrieving results when you no longer need to continue the task.",
|
||||
Name = "BackgroundAgents_ClearCompletedTask",
|
||||
Description = "Remove a completed or failed background task and release its session to free memory. Use this after retrieving results when you no longer need to continue the task.",
|
||||
SerializerOptions = serializerOptions,
|
||||
}),
|
||||
];
|
||||
+7
-7
@@ -8,21 +8,21 @@ using Microsoft.Shared.DiagnosticIds;
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Options controlling the behavior of <see cref="SubAgentsProvider"/>.
|
||||
/// Options controlling the behavior of <see cref="BackgroundAgentsProvider"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class SubAgentsProviderOptions
|
||||
public sealed class BackgroundAgentsProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets custom instructions provided to the agent for using the sub-agent tools.
|
||||
/// Gets or sets custom instructions provided to the agent for using the background agent tools.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use the <c>{sub_agents}</c> placeholder to allow the provider to inject
|
||||
/// the formatted list of available sub agents.
|
||||
/// Use the <c>{background_agents}</c> placeholder to allow the provider to inject
|
||||
/// the formatted list of available background agents.
|
||||
/// </remarks>
|
||||
/// <value>
|
||||
/// When <see langword="null"/> (the default), the provider uses built-in instructions
|
||||
/// that guide the agent on how to use the sub-agent tools.
|
||||
/// that guide the agent on how to use the background agent tools.
|
||||
/// The agent list is always appended after the instructions regardless of this setting.
|
||||
/// </value>
|
||||
public string? Instructions { get; set; }
|
||||
@@ -33,7 +33,7 @@ public sealed class SubAgentsProviderOptions
|
||||
/// <value>
|
||||
/// When <see langword="null"/> (the default), the provider generates a standard list of agent names and descriptions.
|
||||
/// When set, this function receives the dictionary of available agents (keyed by name) and should return
|
||||
/// a formatted string describing the available sub-agents.
|
||||
/// a formatted string describing the available background agents.
|
||||
/// </value>
|
||||
public Func<IReadOnlyDictionary<string, AIAgent>, string>? AgentListBuilder { get; set; }
|
||||
}
|
||||
+9
-9
@@ -7,43 +7,43 @@ using Microsoft.Shared.DiagnosticIds;
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the metadata and result of a sub-task managed by the <see cref="SubAgentsProvider"/>.
|
||||
/// Represents the metadata and result of a background task managed by the <see cref="BackgroundAgentsProvider"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public sealed class SubTaskInfo
|
||||
public sealed class BackgroundTaskInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier for this sub-task.
|
||||
/// Gets or sets the unique identifier for this background task.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the agent that is executing this sub-task.
|
||||
/// Gets or sets the name of the agent that is executing this background task.
|
||||
/// </summary>
|
||||
[JsonPropertyName("agentName")]
|
||||
public string AgentName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a description of what this sub-task is doing.
|
||||
/// Gets or sets a description of what this background task is doing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current status of this sub-task.
|
||||
/// Gets or sets the current status of this background task.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public SubTaskStatus Status { get; set; }
|
||||
public BackgroundTaskStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text result of the sub-task, populated when the task completes successfully.
|
||||
/// Gets or sets the text result of the background task, populated when the task completes successfully.
|
||||
/// </summary>
|
||||
[JsonPropertyName("resultText")]
|
||||
public string? ResultText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error message if the sub-task failed.
|
||||
/// Gets or sets the error message if the background task failed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("errorText")]
|
||||
public string? ErrorText { get; set; }
|
||||
+6
-6
@@ -6,28 +6,28 @@ using Microsoft.Shared.DiagnosticIds;
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the status of a sub-task managed by the <see cref="SubAgentsProvider"/>.
|
||||
/// Represents the status of a background task managed by the <see cref="BackgroundAgentsProvider"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public enum SubTaskStatus
|
||||
public enum BackgroundTaskStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The sub-task is currently running.
|
||||
/// The background task is currently running.
|
||||
/// </summary>
|
||||
Running,
|
||||
|
||||
/// <summary>
|
||||
/// The sub-task completed successfully.
|
||||
/// The background task completed successfully.
|
||||
/// </summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>
|
||||
/// The sub-task failed with an error.
|
||||
/// The background task failed with an error.
|
||||
/// </summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>
|
||||
/// The sub-task's in-flight reference was lost (e.g., after a restart),
|
||||
/// The background task's in-flight reference was lost (e.g., after a restart),
|
||||
/// and its final state cannot be determined.
|
||||
/// </summary>
|
||||
Lost,
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the input for completing a single todo item via the <see cref="TodoProvider"/>.
|
||||
/// </summary>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
internal sealed class TodoCompleteInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the todo item to mark as complete.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the reason describing how or why the item was completed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public sealed class TodoProvider : AIContextProvider, IDisposable
|
||||
|
||||
Use these tools to manage your tasks:
|
||||
- Use TodoList_Add to break down complex work into trackable items (supports adding one or many at once).
|
||||
- Use TodoList_Complete to mark items as done when finished (supports one or many at once).
|
||||
- Use TodoList_Complete to mark items as done when finished (supports one or many at once). Include a reason describing how the items were completed.
|
||||
- Use TodoList_GetRemaining to check what work is still pending.
|
||||
- Use TodoList_GetAll to review the full list including completed items.
|
||||
- Use TodoList_Remove to remove items that are no longer needed (supports one or many at once).
|
||||
@@ -235,14 +235,14 @@ public sealed class TodoProvider : AIContextProvider, IDisposable
|
||||
}),
|
||||
|
||||
AIFunctionFactory.Create(
|
||||
async (List<int> ids) =>
|
||||
async (List<TodoCompleteInput> items) =>
|
||||
{
|
||||
SemaphoreSlim sessionLock = this.GetSessionLock(session);
|
||||
await sessionLock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
TodoState state = this._sessionState.GetOrInitializeState(session);
|
||||
var idSet = new HashSet<int>(ids);
|
||||
var idSet = new HashSet<int>(items.Select(i => i.Id));
|
||||
int completed = 0;
|
||||
foreach (TodoItem item in state.Items)
|
||||
{
|
||||
@@ -268,7 +268,7 @@ public sealed class TodoProvider : AIContextProvider, IDisposable
|
||||
new AIFunctionFactoryOptions
|
||||
{
|
||||
Name = "TodoList_Complete",
|
||||
Description = "Mark one or more todo items as complete by their IDs. Returns the number of items that were found and marked complete.",
|
||||
Description = "Mark one or more todo items as complete. Each entry has an ID and a reason describing how/why the item was completed. Returns the number of items that were found and marked complete.",
|
||||
SerializerOptions = serializerOptions,
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user