-
-
Notifications
You must be signed in to change notification settings - Fork 643
Add ToolProgressIndicator for real-time tool progress updates #1393
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
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| using BotSharp.Abstraction.Routing.Executor; | ||
| using BotSharp.Core.MCP.Managers; | ||
| using BotSharp.Core.MessageHub; | ||
| using ModelContextProtocol; | ||
| using ModelContextProtocol.Protocol; | ||
|
|
||
| namespace BotSharp.Core.Routing.Executor; | ||
|
|
@@ -33,8 +35,12 @@ public async Task<bool> ExecuteAsync(RoleDialogModel message) | |
| return false; | ||
| } | ||
|
|
||
| // Call the tool through mcpdotnet | ||
| var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); | ||
| // Call the tool through mcpdotnet, relaying whatever progress it reports along the | ||
| // way — see ToolProgressIndicator for why the reporter has to be supplied here. | ||
| var result = await client.CallToolAsync( | ||
| _functionName, | ||
| !argDict.IsNullOrEmpty() ? argDict : [], | ||
| progress: ToolProgressIndicator.For(_services, message)); | ||
|
|
||
| // Extract the text content from the result | ||
| var json = string.Join("\n", result.Content.Where(c => c is TextContentBlock).Select(c => ((TextContentBlock)c).Text)); | ||
|
|
@@ -55,6 +61,99 @@ public Task<string> GetIndicatorAsync(RoleDialogModel message) | |
| return Task.FromResult(message.Indication ?? string.Empty); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Turns a tool's <c>notifications/progress</c> into indications, so a long call can say what | ||
| /// it is doing while it does it. | ||
| /// <para> | ||
| /// WHY THIS EXISTS. RoutingService pushes one indication when a function starts, and for a | ||
| /// tool that returns in a second that is the whole story. An MCP tool driving a browser takes | ||
| /// minutes, and that single line — "Working out the steps" — was all the chat had to show for | ||
| /// the entire run: no way to tell a task making progress from one that had wedged. | ||
| /// </para> | ||
| /// <para> | ||
| /// Supplying the reporter is also what MAKES a server report. The SDK only attaches a | ||
| /// <c>progressToken</c> to the request when this is non-null, and a server with no token to | ||
| /// answer has nowhere to send notifications — computer-autoplay's <c>await_web_task</c> | ||
| /// returns early from its own reporter for exactly that reason. So the argument is not an | ||
| /// optimisation of an existing stream; it is what opens it. | ||
| /// </para> | ||
| /// <para> | ||
| /// Nothing is required of a server that does not report progress: no notifications arrive, | ||
| /// <see cref="Report"/> is never called, and the call behaves as it did before. | ||
| /// </para> | ||
| /// </summary> | ||
| private sealed class ToolProgressIndicator : IProgress<ProgressNotificationValue> | ||
| { | ||
| private readonly MessageHub<HubObserveData<RoleDialogModel>> _hub; | ||
| private readonly string _conversationId; | ||
| private readonly RoleDialogModel _message; | ||
|
|
||
| private ToolProgressIndicator( | ||
| MessageHub<HubObserveData<RoleDialogModel>> hub, | ||
| string conversationId, | ||
| RoleDialogModel message) | ||
| { | ||
| _hub = hub; | ||
| _conversationId = conversationId; | ||
| _message = message; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// A reporter for this call, or null when there is no conversation to report into — a | ||
| /// tool invoked outside one, from a task or a test. Null is the right answer there rather | ||
| /// than a reporter that drops everything: it also tells the server not to bother sending. | ||
| /// <para> | ||
| /// The conversation id is read HERE, on the thread that starts the call, and captured. | ||
| /// <see cref="Report"/> runs on whichever thread the MCP transport is reading on, and | ||
| /// resolving a scoped service from there to ask again would be a race for a value that | ||
| /// cannot change during the call. | ||
| /// </para> | ||
| /// </summary> | ||
| public static ToolProgressIndicator? For(IServiceProvider services, RoleDialogModel message) | ||
| { | ||
| var conversationId = services.GetRequiredService<IConversationService>().ConversationId; | ||
| if (string.IsNullOrWhiteSpace(conversationId)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var hub = services.GetRequiredService<MessageHub<HubObserveData<RoleDialogModel>>>(); | ||
| return new ToolProgressIndicator(hub, conversationId, message); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Pushes one indication per reported step. | ||
| /// <para> | ||
| /// Deliberately synchronous, which is why this is a hand-written IProgress rather than a | ||
| /// <see cref="Progress{T}"/>: Progress<T> queues every report to the thread pool | ||
| /// independently, so two steps reported together can arrive out of order — and the one | ||
| /// that arrives last is the one left on screen. Pushing inline keeps the order the server | ||
| /// sent them in. | ||
| /// </para> | ||
| /// </summary> | ||
| public void Report(ProgressNotificationValue value) | ||
| { | ||
| // A bare count with no message is a progress BAR's input, not a sentence, and the | ||
| // chat has nowhere to put it. Servers that only send numbers are simply not relayed. | ||
| if (string.IsNullOrWhiteSpace(value.Message)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Cloned: this is pushed to observers that read it, and the function's own message is | ||
| // still being used by the call in flight. Its indication is not ours to overwrite. | ||
| var indication = RoleDialogModel.From(_message); | ||
| indication.Indication = value.Message; | ||
|
|
||
| _hub.Push(new() | ||
| { | ||
| EventName = ChatEvent.OnIndicationReceived, | ||
| Data = indication, | ||
|
Comment on lines
+148
to
+151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Observer exceptions break tool ToolProgressIndicator.Report pushes progress into MessageHub without guarding against subscriber/listener exceptions; failures in the indication pipeline (e.g., SSE writes when a client disconnects) can propagate back into the MCP progress callback and cause CallToolAsync to fault/abort. This makes tool execution reliability depend on UI/observer health rather than being best-effort telemetry. Agent Prompt
|
||
| RefId = _conversationId | ||
| }); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| private static Dictionary<string, object?> JsonToDictionary(string? json) | ||
| { | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2. Progress blocks tool io
🐞 Bug➹ PerformanceAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools