| | | 1 | | using NexusLabs.Needlr.AgentFramework; |
| | | 2 | | |
| | | 3 | | namespace NexusLabs.Needlr.AgentFramework.Workflows; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Terminates a workflow when an agent calls a specific tool/function during its turn. |
| | | 7 | | /// Unlike <see cref="KeywordTerminationCondition"/> which matches response text, |
| | | 8 | | /// this condition matches on structured tool call data — eliminating false positives |
| | | 9 | | /// from keywords appearing in natural language. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <remarks> |
| | | 12 | | /// <para> |
| | | 13 | | /// This condition inspects <see cref="TerminationContext.ToolCallNames"/> which is |
| | | 14 | | /// populated from <c>FunctionCallContent</c> entries in the agent's response message. |
| | | 15 | | /// The match is exact and case-sensitive on the tool name. |
| | | 16 | | /// </para> |
| | | 17 | | /// <para> |
| | | 18 | | /// Designed for scenarios where an agent should signal approval or completion via a |
| | | 19 | | /// dedicated tool call rather than embedding a keyword in free text. For example, a |
| | | 20 | | /// reviewer agent calls <c>ApproveArticle()</c> instead of writing "APPROVED" — which |
| | | 21 | | /// avoids the problem of the LLM accidentally including the keyword in rejection text. |
| | | 22 | | /// </para> |
| | | 23 | | /// </remarks> |
| | | 24 | | /// <example> |
| | | 25 | | /// <code> |
| | | 26 | | /// // Declare on the reviewer agent class: |
| | | 27 | | /// [AgentGroupChatMember("article-writing")] |
| | | 28 | | /// [AgentTerminationCondition(typeof(ToolCallTerminationCondition), "ApproveArticle", "ReviewerAgent")] |
| | | 29 | | /// public sealed class ReviewerAgent; |
| | | 30 | | /// |
| | | 31 | | /// // The reviewer's instructions tell it to call ApproveArticle() when satisfied: |
| | | 32 | | /// // "If no Critical or Major issues remain, call the ApproveArticle tool." |
| | | 33 | | /// </code> |
| | | 34 | | /// </example> |
| | | 35 | | public sealed class ToolCallTerminationCondition : IWorkflowTerminationCondition |
| | | 36 | | { |
| | | 37 | | private readonly string _toolName; |
| | | 38 | | private readonly string? _agentId; |
| | | 39 | | |
| | | 40 | | /// <summary> |
| | | 41 | | /// Initializes a new instance that fires when <em>any</em> agent calls the |
| | | 42 | | /// specified tool. |
| | | 43 | | /// </summary> |
| | | 44 | | /// <param name="toolName">The exact tool/function name to match (case-sensitive).</param> |
| | | 45 | | public ToolCallTerminationCondition(string toolName) |
| | 9 | 46 | | : this(toolName, agentId: null) |
| | | 47 | | { |
| | 7 | 48 | | } |
| | | 49 | | |
| | | 50 | | /// <summary> |
| | | 51 | | /// Initializes a new instance that fires when a specific agent calls the |
| | | 52 | | /// specified tool. |
| | | 53 | | /// </summary> |
| | | 54 | | /// <param name="toolName">The exact tool/function name to match (case-sensitive).</param> |
| | | 55 | | /// <param name="agentId"> |
| | | 56 | | /// The agent name or executor ID to restrict the match to, or <see langword="null"/> |
| | | 57 | | /// to match any agent. Supports MAF's GUID-suffixed executor IDs via prefix matching. |
| | | 58 | | /// </param> |
| | 12 | 59 | | public ToolCallTerminationCondition(string toolName, string? agentId) |
| | | 60 | | { |
| | 12 | 61 | | ArgumentException.ThrowIfNullOrWhiteSpace(toolName); |
| | 10 | 62 | | _toolName = toolName; |
| | 10 | 63 | | _agentId = agentId; |
| | 10 | 64 | | } |
| | | 65 | | |
| | | 66 | | /// <inheritdoc/> |
| | | 67 | | public bool ShouldTerminate(TerminationContext context) |
| | | 68 | | { |
| | 10 | 69 | | ArgumentNullException.ThrowIfNull(context); |
| | | 70 | | |
| | 9 | 71 | | if (_agentId is not null |
| | 9 | 72 | | && !string.Equals(context.AgentId, _agentId, StringComparison.Ordinal) |
| | 9 | 73 | | && !context.AgentId.StartsWith(_agentId + "_", StringComparison.Ordinal)) |
| | | 74 | | { |
| | 1 | 75 | | return false; |
| | | 76 | | } |
| | | 77 | | |
| | 26 | 78 | | foreach (var toolName in context.ToolCallNames) |
| | | 79 | | { |
| | 7 | 80 | | if (string.Equals(toolName, _toolName, StringComparison.Ordinal)) |
| | 4 | 81 | | return true; |
| | | 82 | | } |
| | | 83 | | |
| | 4 | 84 | | return false; |
| | 4 | 85 | | } |
| | | 86 | | } |