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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<!-- Toggle to prefer local project references in samples/toys -->
<PropertyGroup>
<PreferProjectReferences>false</PreferProjectReferences>
<PreferProjectReferences>true</PreferProjectReferences>
</PropertyGroup>


Expand Down
32 changes: 16 additions & 16 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<ItemGroup>
<!-- Core + DI -->
<PackageVersion Include="Discord.Net" Version="3.18.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
Expand All @@ -29,18 +29,18 @@
<PackageVersion Include="NuGet.Common" Version="6.14.0" />
<PackageVersion Include="NuGet.Packaging" Version="6.14.0" />
<PackageVersion Include="NuGet.Protocol" Version="6.14.0" />
<!-- OpenAI official .NET SDK -->
<PackageVersion Include="OpenAI" Version="2.5.0" />
<!-- Coven packages (centralized) -->
<PackageVersion Include="Coven.Core" Version="2.0.1" />
<PackageVersion Include="Coven.Core.Streaming" Version="2.0.1" />
<PackageVersion Include="Coven.Daemonology" Version="2.0.1" />
<PackageVersion Include="Coven.Transmutation" Version="2.0.1" />
<PackageVersion Include="Coven.Spellcasting" Version="2.0.1" />
<PackageVersion Include="Coven.Chat" Version="2.0.1" />
<PackageVersion Include="Coven.Chat.Console" Version="2.0.1" />
<PackageVersion Include="Coven.Chat.Discord" Version="2.0.1" />
<PackageVersion Include="Coven.Agents" Version="2.0.1" />
<PackageVersion Include="Coven.Agents.OpenAI" Version="2.0.1" />
</ItemGroup>
</Project>
<!-- OpenAI official .NET SDK -->
<PackageVersion Include="OpenAI" Version="2.5.0" />
<!-- Coven packages (centralized) -->
<PackageVersion Include="Coven.Core" Version="2.0.1" />
<PackageVersion Include="Coven.Core.Streaming" Version="2.0.1" />
<PackageVersion Include="Coven.Daemonology" Version="2.0.1" />
<PackageVersion Include="Coven.Transmutation" Version="2.0.1" />
<PackageVersion Include="Coven.Spellcasting" Version="2.0.1" />
<PackageVersion Include="Coven.Chat" Version="2.0.1" />
<PackageVersion Include="Coven.Chat.Console" Version="2.0.1" />
<PackageVersion Include="Coven.Chat.Discord" Version="2.0.1" />
<PackageVersion Include="Coven.Agents" Version="2.0.1" />
<PackageVersion Include="Coven.Agents.OpenAI" Version="2.0.1" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Project overview: see [README](/README.md).
- Solution: [/src/Coven.sln](/src/Coven.sln)

- Core: [/src/Coven.Core](/src/Coven.Core/) ([README](/src/Coven.Core/README.md))
- Core Debug: [/src/Coven.Core.Debug](/src/Coven.Core.Debug/) ([README](/src/Coven.Core.Debug/README.md))
- Streaming: [/src/Coven.Core.Streaming](/src/Coven.Core.Streaming/) ([README](/src/Coven.Core.Streaming/README.md))
- Daemonology: [/src/Coven.Daemonology](/src/Coven.Daemonology/) ([README](/src/Coven.Daemonology/README.md))
- Transmutation: [/src/Coven.Transmutation](/src/Coven.Transmutation/) ([README](/src/Coven.Transmutation/README.md))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace Coven.Agents.OpenAI;
/// Dependency Injection helpers for wiring the OpenAI agent integration.
/// Registers journals, gateway connection, transmuters, windowing daemons, and the official OpenAI client.
/// </summary>
public static class ServiceCollectionExtensions
public static class OpenAIAgentsServiceCollectionExtensions
{
/// <summary>
/// Registers OpenAI agents with required defaults.
Expand Down Expand Up @@ -75,7 +75,7 @@ public static IServiceCollection AddOpenAIAgents(this IServiceCollection service
});

// Journals and gateway
services.TryAddSingleton<IScrivener<AgentEntry>, InMemoryScrivener<AgentEntry>>();
services.TryAddScoped<IScrivener<AgentEntry>, InMemoryScrivener<AgentEntry>>();
services.AddKeyedScoped<IScrivener<OpenAIEntry>, InMemoryScrivener<OpenAIEntry>>("Coven.InternalOpenAIScrivener");
services.AddScoped<IScrivener<OpenAIEntry>, OpenAIScrivener>();
if (registration.StreamingEnabled)
Expand Down
37 changes: 18 additions & 19 deletions src/Coven.Agents.OpenAI/OpenAIScrivener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,46 @@

namespace Coven.Agents.OpenAI;

internal sealed class OpenAIScrivener : IScrivener<OpenAIEntry>
/// <summary>
/// OpenAI scrivener wrapper that forwards outbound efferent entries to the OpenAI gateway
/// and persists all entries to the inner journal; logs the append for observability.
/// </summary>
internal sealed class OpenAIScrivener : TappedScrivener<OpenAIEntry>
{
private readonly IScrivener<OpenAIEntry> _inner;
private readonly IOpenAIGatewayConnection _gateway;
private readonly ILogger _logger;

/// <summary>
/// Creates an instance wrapping a keyed inner scrivener and an OpenAI gateway connection.
/// </summary>
/// <param name="inner">The keyed inner scrivener used for storage.</param>
/// <param name="gateway">Gateway connection for sending OpenAI requests.</param>
/// <param name="logger">Logger for diagnostic breadcrumbs.</param>
public OpenAIScrivener(
[FromKeyedServices("Coven.InternalOpenAIScrivener")] IScrivener<OpenAIEntry> inner,
IOpenAIGatewayConnection gateway,
ILogger<OpenAIScrivener> logger)
: base(inner)
{
ArgumentNullException.ThrowIfNull(inner);
ArgumentNullException.ThrowIfNull(gateway);
ArgumentNullException.ThrowIfNull(logger);
_inner = inner;
_gateway = gateway;
_logger = logger;
}

public async Task<long> WriteAsync(OpenAIEntry entry, CancellationToken cancellationToken = default)
/// <summary>
/// Sends <see cref="OpenAIEfferent"/> entries via the gateway and appends all entries to the inner scrivener;
/// logs the append with the assigned position.
/// </summary>
public override async Task<long> WriteAsync(OpenAIEntry entry, CancellationToken cancellationToken = default)
{
if (entry is OpenAIEfferent outgoing)
{
await _gateway.SendAsync(outgoing, cancellationToken).ConfigureAwait(false);
}

long pos = await _inner.WriteAsync(entry, cancellationToken).ConfigureAwait(false);
long pos = await WriteInnerAsync(entry, cancellationToken).ConfigureAwait(false);
OpenAILog.OpenAIScrivenerAppended(_logger, entry.GetType().Name, pos);
return pos;
}

public IAsyncEnumerable<(long journalPosition, OpenAIEntry entry)> TailAsync(long afterPosition = 0, CancellationToken cancellationToken = default)
=> _inner.TailAsync(afterPosition, cancellationToken);

public IAsyncEnumerable<(long journalPosition, OpenAIEntry entry)> ReadBackwardAsync(long beforePosition = long.MaxValue, CancellationToken cancellationToken = default)
=> _inner.ReadBackwardAsync(beforePosition, cancellationToken);

public Task<(long journalPosition, OpenAIEntry entry)> WaitForAsync(long afterPosition, Func<OpenAIEntry, bool> match, CancellationToken cancellationToken = default)
=> _inner.WaitForAsync(afterPosition, match, cancellationToken);

public Task<(long journalPosition, TDerived entry)> WaitForAsync<TDerived>(long afterPosition, Func<TDerived, bool> match, CancellationToken cancellationToken = default)
where TDerived : OpenAIEntry
=> _inner.WaitForAsync(afterPosition, match, cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// SPDX-License-Identifier: BUSL-1.1

using Coven.Core;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Coven.Daemonology;
using Coven.Transmutation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Coven.Chat.Console;

/// <summary>
/// Dependency Injection helpers for wiring the Console chat adapter.
/// Registers gateway/session components, journals, the Console↔Chat transmuter, and the console daemon.
/// </summary>
public static class ServiceCollectionExtensions
public static class ConsoleChatServiceCollectionExtensions
{
/// <summary>
/// Adds Console chat integration using the provided client configuration.
Expand All @@ -29,7 +29,7 @@ public static IServiceCollection AddConsoleChat(this IServiceCollection services
services.AddScoped<ConsoleChatSessionFactory>();

// Default ChatEntry journal if none provided by host
services.TryAddSingleton<IScrivener<ChatEntry>, InMemoryScrivener<ChatEntry>>();
services.TryAddScoped<IScrivener<ChatEntry>, InMemoryScrivener<ChatEntry>>();

services.AddScoped<IScrivener<ConsoleEntry>, ConsoleScrivener>();
services.AddKeyedScoped<IScrivener<ConsoleEntry>, InMemoryScrivener<ConsoleEntry>>("Coven.InternalConsoleScrivener");
Expand Down
32 changes: 12 additions & 20 deletions src/Coven.Chat.Console/ConsoleScrivener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,40 @@

namespace Coven.Chat.Console;

internal sealed class ConsoleScrivener : IScrivener<ConsoleEntry>
/// <summary>
/// Console chat scrivener wrapper that forwards outbound efferent entries to the console gateway
/// and persists all entries to the inner journal for deterministic ordering and observation.
/// </summary>
internal sealed class ConsoleScrivener : TappedScrivener<ConsoleEntry>
{
private readonly IScrivener<ConsoleEntry> _scrivener;
private readonly ConsoleGatewayConnection _gateway;
private readonly ILogger _logger;

public ConsoleScrivener(
[FromKeyedServices("Coven.InternalConsoleScrivener")] IScrivener<ConsoleEntry> scrivener,
ConsoleGatewayConnection gateway,
ILogger<ConsoleScrivener> logger)
: base(scrivener)
{
ArgumentNullException.ThrowIfNull(scrivener);
ArgumentNullException.ThrowIfNull(gateway);
ArgumentNullException.ThrowIfNull(logger);
_scrivener = scrivener;
_gateway = gateway;
_logger = logger;
}

public async Task<long> WriteAsync(ConsoleEntry entry, CancellationToken cancellationToken = default)
/// <summary>
/// Sends <see cref="ConsoleEfferent"/> entries to the console gateway and appends
/// all entries to the inner scrivener; logs the append with the assigned position.
/// </summary>
public override async Task<long> WriteAsync(ConsoleEntry entry, CancellationToken cancellationToken = default)
{
if (entry is ConsoleEfferent)
{
await _gateway.SendAsync(entry.Text, cancellationToken).ConfigureAwait(false);
}

long pos = await _scrivener.WriteAsync(entry, cancellationToken).ConfigureAwait(false);
long pos = await WriteInnerAsync(entry, cancellationToken).ConfigureAwait(false);
ConsoleLog.ConsoleScrivenerAppended(_logger, entry.GetType().Name, pos);
return pos;
}

public IAsyncEnumerable<(long journalPosition, ConsoleEntry entry)> TailAsync(long afterPosition = 0, CancellationToken cancellationToken = default)
=> _scrivener.TailAsync(afterPosition, cancellationToken);

public IAsyncEnumerable<(long journalPosition, ConsoleEntry entry)> ReadBackwardAsync(long beforePosition = long.MaxValue, CancellationToken cancellationToken = default)
=> _scrivener.ReadBackwardAsync(beforePosition, cancellationToken);

public Task<(long journalPosition, ConsoleEntry entry)> WaitForAsync(long afterPosition, Func<ConsoleEntry, bool> match, CancellationToken cancellationToken = default)
=> _scrivener.WaitForAsync(afterPosition, match, cancellationToken);

public Task<(long journalPosition, TDerived entry)> WaitForAsync<TDerived>(long afterPosition, Func<TDerived, bool> match, CancellationToken cancellationToken = default)
where TDerived : ConsoleEntry
=> _scrivener.WaitForAsync(afterPosition, match, cancellationToken);
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Coven.Chat.Discord;
/// Dependency Injection helpers for wiring the Discord chat adapter.
/// Registers the Discord client, session factory, journals, transmuter, daemon, and default windowing policies.
/// </summary>
public static class ServiceCollectionExtensions
public static class DiscordChatServiceCollectionExtensions
{
/// <summary>
/// Adds Discord chat integration using the provided client configuration.
Expand Down Expand Up @@ -45,7 +45,7 @@ public static IServiceCollection AddDiscordChat(this IServiceCollection services
services.AddScoped<DiscordGatewayConnection>();

// Default ChatEntry journal if none provided by host
services.TryAddSingleton<IScrivener<ChatEntry>, InMemoryScrivener<ChatEntry>>();
services.TryAddScoped<IScrivener<ChatEntry>, InMemoryScrivener<ChatEntry>>();

services.AddScoped<IScrivener<DiscordEntry>, DiscordScrivener>();
services.AddKeyedScoped<IScrivener<DiscordEntry>, InMemoryScrivener<DiscordEntry>>("Coven.InternalDiscordScrivener");
Expand Down
35 changes: 14 additions & 21 deletions src/Coven.Chat.Discord/DiscordScrivener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,36 @@

namespace Coven.Chat.Discord;

internal sealed class DiscordScrivener : IScrivener<DiscordEntry>
/// <summary>
/// Discord chat scrivener wrapper that forwards outbound efferent entries to Discord via the gateway
/// and persists all entries to the inner journal so pumps/tests can observe ordering.
/// </summary>
internal sealed class DiscordScrivener : TappedScrivener<DiscordEntry>
{
private readonly IScrivener<DiscordEntry> _scrivener;
private readonly DiscordGatewayConnection _discordClient;
private readonly ILogger _logger;

/// <summary>
/// Wraps a keyed inner scrivener and forwards outbound efferent messages to Discord.
/// </summary>
/// <param name="scrivener">The keyed inner <see cref="IScrivener{TJournalEntryType}"/> used for storage.</param>
/// <param name="scrivener">The keyed inner scrivener used for storage.</param>
/// <param name="discordClient">The gateway connection for sending messages to Discord.</param>
/// <param name="logger">Logger for diagnostic breadcrumbs.</param>
/// <remarks> Because we are what we utilize, ensure that the inner scrivener is keyed in DI.</remarks>
/// <remarks>Ensure the inner scrivener is keyed in DI.</remarks>
public DiscordScrivener([FromKeyedServices("Coven.InternalDiscordScrivener")] IScrivener<DiscordEntry> scrivener, DiscordGatewayConnection discordClient, ILogger<DiscordScrivener> logger)
: base(scrivener)
{
ArgumentNullException.ThrowIfNull(scrivener);
ArgumentNullException.ThrowIfNull(discordClient);
ArgumentNullException.ThrowIfNull(logger);
_scrivener = scrivener;
_discordClient = discordClient;
_logger = logger;
}

public async Task<long> WriteAsync(DiscordEntry entry, CancellationToken cancellationToken = default)
/// <summary>
/// Sends <see cref="DiscordEfferent"/> entries to Discord via the gateway and appends
/// all entries to the inner scrivener; logs the append with the assigned position.
/// </summary>
public override async Task<long> WriteAsync(DiscordEntry entry, CancellationToken cancellationToken = default)
{
// Only send actual outbound messages to Discord. ACKs and inbound entries must not be sent.
if (entry is DiscordEfferent)
Expand All @@ -38,21 +44,8 @@ public async Task<long> WriteAsync(DiscordEntry entry, CancellationToken cancell
}

// Always persist to the underlying scrivener so pumps and tests can observe ordering.
long pos = await _scrivener.WriteAsync(entry, cancellationToken).ConfigureAwait(false);
long pos = await WriteInnerAsync(entry, cancellationToken).ConfigureAwait(false);
DiscordLog.DiscordScrivenerAppended(_logger, entry.GetType().Name, pos);
return pos;
}

public IAsyncEnumerable<(long journalPosition, DiscordEntry entry)> TailAsync(long afterPosition = 0, CancellationToken cancellationToken = default)
=> _scrivener.TailAsync(afterPosition, cancellationToken);

public IAsyncEnumerable<(long journalPosition, DiscordEntry entry)> ReadBackwardAsync(long beforePosition = long.MaxValue, CancellationToken cancellationToken = default)
=> _scrivener.ReadBackwardAsync(beforePosition, cancellationToken);

public Task<(long journalPosition, DiscordEntry entry)> WaitForAsync(long afterPosition, Func<DiscordEntry, bool> match, CancellationToken cancellationToken = default)
=> _scrivener.WaitForAsync(afterPosition, match, cancellationToken);

public Task<(long journalPosition, TDerived entry)> WaitForAsync<TDerived>(long afterPosition, Func<TDerived, bool> match, CancellationToken cancellationToken = default)
where TDerived : DiscordEntry
=> _scrivener.WaitForAsync(afterPosition, match, cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Coven.Chat;
/// Adds generic chat windowing infrastructure (journal, daemon) with a DI-provided window policy.
/// Useful for chunking and emitting grouped <see cref="ChatChunk"/> as <see cref="ChatEfferent"/> entries.
/// </summary>
public static class ServiceCollectionExtensions
public static class ChatWindowingServiceCollectionExtensions
{
/// <summary>
/// Registers chat windowing components and a windowing daemon for <see cref="ChatEntry"/>.
Expand All @@ -26,7 +26,7 @@ public static IServiceCollection AddChatWindowing(this IServiceCollection servic
ArgumentNullException.ThrowIfNull(services);

// Ensure required journals exist
services.TryAddSingleton<IScrivener<ChatEntry>, InMemoryScrivener<ChatEntry>>();
services.TryAddScoped<IScrivener<ChatEntry>, InMemoryScrivener<ChatEntry>>();
services.TryAddSingleton<IScrivener<DaemonEvent>, InMemoryScrivener<DaemonEvent>>();

// Register generic windowing daemon for Chat using a DI-provided policy
Expand Down
19 changes: 19 additions & 0 deletions src/Coven.Core.Debug/Coven.Core.Debug.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>true</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>$(MSBuildProjectName)</PackageId>
<Description>Diagnostics helpers for Coven.Core (e.g., scrivener dump finalizer).</Description>
<Authors>Coven</Authors>
<PackageTags>coven;debug;diagnostics;journal;dump</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Coven.Core/Coven.Core.csproj" />
</ItemGroup>
</Project>

25 changes: 25 additions & 0 deletions src/Coven.Core.Debug/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Coven.Core.Debug — Scrivener Taps

Minimal, zero‑ceremony way to observe journal writes without changing any branch/leaf code: wrap an existing `IScrivener<T>` with a tiny decorator that calls an observer delegate after each write.

## Goal
- Add an observer delegate to an existing scrivener.
- Do not change `IScrivener<T>`.
- Do not write back to the observed journal.
- Preserve normal tail/read behavior and positions.

## Concept
- `TappedScrivener<TEntry>` has an inner `IScrivener<TEntry>`.
- On `WriteAsync(entry)`, it awaits the inner write to get the assigned `position`, then invokes an observer delegate: `Action<long, TEntry>`.
- All read APIs (`TailAsync`, `ReadBackwardAsync`, `WaitForAsync`) simply delegate to the inner scrivener.

## Behavior
- Observation happens after the inner write completes, using the actual assigned position.
- Observer exceptions are ignored (best‑effort, non‑interfering).
- No writes back to the observed journal; tap is read‑only aside from the delegated call.
- Overhead is a single delegate invocation per write.

## Notes
- Keep observers cheap; they run inline after writes. If you need heavy work, queue it yourself inside the delegate.
- Because it wraps a specific IScrivener<T> instance, it only observes entries written through that instance.
- Because we need to register the TappedScrivener as the final implementation for IScrivener<TEntry> we will need some way to disambiguate them.
Loading