Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions config/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,7 @@ tools:
- { id: search, kind: builtin, uses: web.search }

# kind: builtin, a plain function. The model calls it when a file in the conversation's workspace is a
# deliverable: the file is copied into providers.blobs (see 8. providers), owned by the conversation,
# and the person gets a link. Files not published are lost when the conversation ends. Needs
# providers.blobs and options.UseWorkspace(...) on the host; missing either fails the boot.
# deliverable: the file is copied into providers.blobs.
- { id: publish, kind: builtin, uses: file.publish }

# kind: binding, typed form. Not used in this example. The host registered
Expand Down
2 changes: 1 addition & 1 deletion src/AgentCore.Application/Blobs/BlobLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace AgentCore.Application.Blobs
public static class BlobLink
{
/// <summary>How long a link works.</summary>
public static TimeSpan Lifetime { get; } = TimeSpan.FromMinutes(15);
public static TimeSpan Lifetime { get; } = TimeSpan.FromMinutes(5);

/// <summary>
/// The <c>Content-Disposition</c> a link serves the blob with: <c>inline</c> for what a browser
Expand Down
16 changes: 16 additions & 0 deletions src/AgentCore.Application/Blobs/BlobOwner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace AgentCore.Application.Blobs
{
/// <summary>
/// The one shape a blob owner id may take. An owner id is the first segment of a key, never a path.
/// </summary>
public static class BlobOwner
{
/// <summary>Says whether an owner id is safe to use as the first segment of a key.</summary>
/// <param name="ownerId">The conversation id or other owner the caller wrote.</param>
/// <returns><see langword="true"/> when the id is not blank and holds no slash.</returns>
public static bool IsSafe(string? ownerId)
{
return !string.IsNullOrWhiteSpace(ownerId) && !ownerId.Contains('/', StringComparison.Ordinal);
}
}
}
7 changes: 0 additions & 7 deletions src/AgentCore.Application/Blobs/FileLink.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private static void Add(
if (context.ChatClients.ResolveHostedTool(tool, model) is not { } resolved)
{
ILogger logger = context.Loggers?.CreateLogger(typeof(AgentToolCompiler)) ?? NullLogger.Instance;

string modelDescription = model is { Ref.Length: > 0 } ? $"the model '{model.Ref}'" : "this agent's default model";

Log.HostedToolDropped(logger, agentId, toolId, tool.GetType().Name, modelDescription);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal override EntryBuild BuildEntry(
StageConfiguration stage = policy.Stages[index];
string stagePointer = ConfigurationError.AppendPointer(
ConfigurationError.AppendPointer(policyPointer, "stages"), index);

if (stage.Agent is not { } agentId)
{
stages[stage.Id] = NoAgentId;
Expand Down
29 changes: 16 additions & 13 deletions src/AgentCore.Application/Conversation/Conversations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public Conversations(IConversationStore conversations, IBlobStore? blobs)

IReadOnlyList<ConversationMessage> messages = await Store.ReadWindowAsync(conversationId, window, cancellationToken).ConfigureAwait(false);

IReadOnlyList<FileLink> files = await LinkFilesAsync(conversationId, messages.Select(message => message.Content), cancellationToken).ConfigureAwait(false);
IReadOnlyList<BlobRef> files = KeptFiles(conversationId, messages.Select(message => message.Content));

int turns = messages.Select(message => message.TurnIndex).Distinct().Count();

Expand All @@ -55,15 +55,11 @@ public Conversations(IConversationStore conversations, IBlobStore? blobs)
};
}

/// <summary>Links every published file the messages carry and the store kept, each name once, in first-seen order.</summary>
/// <summary>Lists every published file the messages carry and the store kept, each name once, in first-seen order.</summary>
/// <param name="conversationId">The conversation that owns the files.</param>
/// <param name="messages">The messages to read the references off: the stored transcript, or one turn's updates.</param>
/// <param name="cancellationToken">Cancels the signing.</param>
/// <returns>One link per kept file.</returns>
public async Task<IReadOnlyList<FileLink>> LinkFilesAsync(
string conversationId,
IEnumerable<ChatMessage> messages,
CancellationToken cancellationToken = default)
/// <returns>The facts of each kept file. The last mention of a name wins its place's facts.</returns>
internal IReadOnlyList<BlobRef> KeptFiles(string conversationId, IEnumerable<ChatMessage> messages)
{
ArgumentException.ThrowIfNullOrWhiteSpace(conversationId);
ArgumentNullException.ThrowIfNull(messages);
Expand Down Expand Up @@ -95,15 +91,22 @@ public async Task<IReadOnlyList<FileLink>> LinkFilesAsync(
}
}

List<FileLink> links = [];
return kept;
}

foreach (BlobRef blob in kept)
/// <inheritdoc />
public async ValueTask<Uri?> LinkFileAsync(string conversationId, string name, CancellationToken cancellationToken = default)
{
if (_blobs is null || !BlobOwner.IsSafe(conversationId) || !BlobName.IsSafe(name))
{
Uri? url = await _blobs.LinkAsync(blob, BlobLink.Lifetime, cancellationToken).ConfigureAwait(false);
links.Add(new FileLink(blob, url));
return null;
}

return links;
BlobRef? blob = await _blobs.StatAsync(conversationId, name, cancellationToken).ConfigureAwait(false);

return blob is null
? null
: await _blobs.LinkAsync(blob, BlobLink.Lifetime, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc />
Expand Down
7 changes: 4 additions & 3 deletions src/AgentCore.Application/Conversation/StoredConversation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AgentCore.Application.Blobs;
using AgentCore.Application.Ports;
using AgentCore.Application.Transcript;

namespace AgentCore.Application.Conversation
Expand All @@ -7,13 +8,13 @@ namespace AgentCore.Application.Conversation
/// <param name="Conversation">The row.</param>
/// <param name="Messages">The stored messages, oldest first: every one, or one window's worth.</param>
/// <param name="Files">
/// One link per file the messages name and the blob store kept, each name once, in first-seen order.
/// The links expire; read them out at once rather than keeping this record around.
/// The facts of each file the messages name and the blob store kept, each name once, in first-seen order.
/// A link to the bytes comes from <see cref="IConversations.LinkFileAsync"/> when the person asks for one.
/// </param>
public sealed record StoredConversation(
ConversationRecord Conversation,
IReadOnlyList<ConversationMessage> Messages,
IReadOnlyList<FileLink> Files)
IReadOnlyList<BlobRef> Files)
{
/// <summary>
/// Gets the turn to read the next older window before, or <see langword="null"/> when
Expand Down
Loading
Loading