Improved how the AI remembers its earlier tool calls (#1185)

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Peer HogeterpandThorsten Sommer authored and GitHub committed 2026-11-12 19:57:28 +01:00
1 parent 0885862150
commit 382db4b8d0
22 files changed
+1323 -24

No files matched your search

+89 -5
View File
@@ -57,6 +57,45 @@ public sealed class ContentText : IContent
public List<ToolInvocationTrace> ToolInvocations { get; set; } = [];
/// <summary>
/// The web addresses the tools returned while this answer came about and which a later message
/// still shows the model, as request keys, see WebAddresses.CreateRequestKey.
/// </summary>
/// <remarks>
/// Stored with the answer, so that the model may read them again in a later message, also after
/// a restart, while its free address choice is off, see ChatThread.IsWebAddressGivenToTheModel.
/// A tool returned them, so the model neither made them up nor wrote anything of the chat into
/// them; addresses it wrote into the call itself are left out, see ToolCallOutcome.ReturnedWebAddresses.<br/><br/>
///
/// Only those the model can still see later are kept: in the text of the answer, among its
/// sources, or in the arguments of a call which ran, see EndToolRun. A page a tool read returns
/// all of its links, often hundreds. Once the result is gone, the model has no way of knowing
/// them, short of reading the page again, which returns them once more. Storing them all would
/// only make the chat grow.<br/><br/>
///
/// Kept per answer rather than per chat: an answer which is generated anew takes its addresses
/// along, and so does one which is deleted, once the chat is opened again, see
/// ChatThread.RuntimeWebAddressesFromTools.
/// </remarks>
public HashSet<string> WebAddressesFromTools { get; set; } = new(StringComparer.Ordinal);
/// <summary>
/// The web addresses the tools of the running request returned so far, as request keys.
/// </summary>
/// <remarks>
/// Never stored: a chat is saved while its answer is still being written, and would hold every
/// link of every page read until then. EndToolRun keeps those which a later message shows, in
/// WebAddressesFromTools.
/// </remarks>
[JsonIgnore]
public HashSet<string> PendingWebAddressesFromTools { get; private set; } = new(StringComparer.Ordinal);
/// <summary>
/// Notes the web addresses a tool returned for this answer, see PendingWebAddressesFromTools.
/// </summary>
/// <param name="requestKeys">The addresses as request keys, see ToolCallOutcome.ReturnedWebAddresses.</param>
public void KeepWebAddressesFromTools(IEnumerable<string> requestKeys) => this.PendingWebAddressesFromTools.UnionWith(requestKeys);
/// <summary>
/// Optional structured data for a specialized visual presentation of this text.
/// </summary>
@@ -231,13 +270,15 @@ public sealed class ContentText : IContent
/// Clears what the previous run of the tools left behind.
/// </summary>
/// <remarks>
/// Both parts at once, because both belong to one request: the traces the user reads and the
/// payload the counting needs. They were cleared separately for exactly as long as there was
/// only one of them.
/// All parts at once, because all belong to one request: the traces the user reads, the
/// payload the counting needs, and the web addresses the tools returned. They were cleared
/// separately for exactly as long as there was only one of them.
/// </remarks>
public void BeginToolRun()
{
this.ToolInvocations.Clear();
this.WebAddressesFromTools = new(StringComparer.Ordinal);
this.PendingWebAddressesFromTools = new(StringComparer.Ordinal);
this.PendingToolConversation = [];
}
@@ -246,9 +287,50 @@ public sealed class ContentText : IContent
/// </summary>
/// <remarks>
/// The traces stay -- they are what the user reads afterwards to see how the answer came
/// about. What goes is the payload, which belonged to a request that is over.
/// about. What goes is the payload, which belonged to a request that is over.<br/><br/>
///
/// Of the web addresses the tools returned, the answer keeps those which a later message still
/// shows the model, see WebAddressesFromTools. The set is replaced as a whole, since the chat
/// may be saved meanwhile. Whoever ends a request may call this once more, which keeps what the
/// first call kept.
/// </remarks>
public void EndToolRun() => this.PendingToolConversation = [];
public void EndToolRun()
{
this.PendingToolConversation = [];
var shownLater = this.FindRequestKeysShownLater();
var kept = new HashSet<string>(StringComparer.Ordinal);
kept.UnionWith(this.WebAddressesFromTools.Where(shownLater.Contains));
kept.UnionWith(this.PendingWebAddressesFromTools.Where(shownLater.Contains));
this.WebAddressesFromTools = kept;
this.PendingWebAddressesFromTools = new(StringComparer.Ordinal);
}
/// <summary>
/// The request keys of the web addresses a later message shows the model with this answer.
/// </summary>
/// <remarks>
/// Those in its text, those of its sources, and those in the arguments of its calls which ran,
/// as the note in front of the answer names them, see ToolCallHistoryNote. The values of
/// sensitive arguments do not count, since the note hides them.
/// </remarks>
private HashSet<string> FindRequestKeysShownLater()
{
var texts = new List<string> { this.Text };
texts.AddRange(this.Sources.Select(source => source.URL));
foreach (var trace in this.ToolInvocations.Where(trace => trace is { Status: ToolInvocationTraceStatus.SUCCESS, WasExecuted: true }))
texts.AddRange(trace.FindKeptTexts());
var requestKeys = new HashSet<string>(StringComparer.Ordinal);
foreach (var address in texts.SelectMany(WebAddresses.Find))
{
if (WebAddresses.TryCreateRequestKey(address, out var requestKey))
requestKeys.Add(requestKey);
}
return requestKeys;
}
/// <summary>
/// Keeps what the provider says the request behind this answer cost.
@@ -550,6 +632,7 @@ public sealed class ContentText : IContent
Sources = [..this.Sources],
SourcePassages = [..this.SourcePassages],
FileAttachments = [..this.FileAttachments],
WebAddressesFromTools = new HashSet<string>(this.WebAddressesFromTools, StringComparer.Ordinal),
Presentation = this.Presentation?.DeepClone(),
ToolInvocations = [..this.ToolInvocations.Select(x => new ToolInvocationTrace
{
@@ -562,6 +645,7 @@ public sealed class ContentText : IContent
WasExecuted = x.WasExecuted,
StatusMessage = x.StatusMessage,
Arguments = new Dictionary<string, string>(x.Arguments, StringComparer.Ordinal),
ArgumentsJson = x.ArgumentsJson,
Result = x.Result,
JsonResult = x.JsonResult?.DeepClone(),
})],