Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
440404a
fix: refactor actor store usage and improve error handling in chat en…
eanzhao Apr 8, 2026
66e5373
refactor(streaming-proxy): improve error handling and logging in endp…
eanzhao Apr 8, 2026
74aa54a
feat: add IStreamingProxyParticipantStore with chrono-storage impleme…
eanzhao Apr 8, 2026
9564105
feat(actors): migrate persistent stores to GAgent actors
eanzhao Apr 8, 2026
bb5a4c1
feat: introduce persistent readmodel actors for GAgent state projection
eanzhao Apr 9, 2026
e3040b6
feat(agents): ensure read model actors exist before pushing updates
eanzhao Apr 9, 2026
c2638f4
refactor(actor-backed): extract shared command dispatch and readmodel…
eanzhao Apr 9, 2026
1103183
feat(projection): introduce projection system for current-state read …
eanzhao Apr 9, 2026
58c6010
feat: add studio tests and improve backpressure handling
eanzhao Apr 10, 2026
a483511
refactor(user-config): replace actor-backed store with CQRS projection
eanzhao Apr 10, 2026
e63c36b
chore: ignore local configuration files
eanzhao Apr 10, 2026
adb669f
Merge remote-tracking branch 'origin/dev' into fix/agent-store
eanzhao Apr 14, 2026
0525025
fix(role-catalog): add missing workspace sync for draft save/delete
eanzhao Apr 14, 2026
03ff9b3
test(catalog): add ConnectorCatalog and RoleCatalog state transition …
eanzhao Apr 14, 2026
5a34482
test(stores): add adapter tests for all actor-backed stores
eanzhao Apr 14, 2026
003b2e8
docs(role-catalog): update doc comment to reflect draft backup delega…
eanzhao Apr 14, 2026
e2abd68
test(catalog): cover null-draft SaveDraft transitions
eanzhao Apr 14, 2026
698902f
test(stores): deepen adapter test coverage per Codex review
eanzhao Apr 14, 2026
97a70ac
Fix streaming proxy endpoint consistency
eanzhao Apr 14, 2026
2d8b869
Merge remote-tracking branch 'origin/dev' into fix/agent-store
eanzhao Apr 14, 2026
5c99e1e
Fix Codecov patch coverage upload
eanzhao Apr 14, 2026
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ jobs:
if: ${{ success() && env.CODECOV_TOKEN != '' }}
uses: codecov/codecov-action@v5
with:
files: ./artifacts/coverage/**/report/Cobertura.xml
files: ./artifacts/coverage/**/raw/**/coverage.cobertura.xml
disable_search: true
fail_ci_if_error: true
flags: ci
Expand Down
9 changes: 8 additions & 1 deletion aevatar.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
<Folder Name="/agents/">
<Project Path="agents\Aevatar.GAgents.NyxidChat\Aevatar.GAgents.NyxidChat.csproj" />
<Project Path="agents\Aevatar.GAgents.StreamingProxy\Aevatar.GAgents.StreamingProxy.csproj" />
<Project Path="agents/Aevatar.GAgents.Registry/Aevatar.GAgents.Registry.csproj" />
<Project Path="agents/Aevatar.GAgents.ConnectorCatalog/Aevatar.GAgents.ConnectorCatalog.csproj" />
<Project Path="agents/Aevatar.GAgents.RoleCatalog/Aevatar.GAgents.RoleCatalog.csproj" />
<Project Path="agents/Aevatar.GAgents.StreamingProxyParticipant/Aevatar.GAgents.StreamingProxyParticipant.csproj" />
<Project Path="agents/Aevatar.GAgents.UserConfig/Aevatar.GAgents.UserConfig.csproj" />
<Project Path="agents/Aevatar.GAgents.UserMemory/Aevatar.GAgents.UserMemory.csproj" />
<Project Path="agents\Aevatar.GAgents.ChatbotClassifier\Aevatar.GAgents.ChatbotClassifier.csproj" />
<Project Path="agents\Aevatar.GAgents.ChatHistory\Aevatar.GAgents.ChatHistory.csproj" />
<Project Path="agents\Aevatar.GAgents.ChannelRuntime\Aevatar.GAgents.ChannelRuntime.csproj" />
<Project Path="agents\Aevatar.GAgents.Household\Aevatar.GAgents.Household.csproj" />
</Folder>
Expand All @@ -23,7 +30,6 @@
<File Path="docs\PROJECT_SPLIT_STRATEGY.md" />
<File Path="docs\STREAM_FORWARD_ARCHITECTURE.md" />
</Folder>
<Folder Name="/docs/00-overview/" />
<Folder Name="/resources/">
<File Path="Directory.Packages.props" />
<File Path="Directory.Build.props" />
Expand Down Expand Up @@ -99,6 +105,7 @@
<Project Path="src/Aevatar.Studio.Application/Aevatar.Studio.Application.csproj" />
<Project Path="src/Aevatar.Studio.Infrastructure/Aevatar.Studio.Infrastructure.csproj" />
<Project Path="src/Aevatar.Studio.Hosting/Aevatar.Studio.Hosting.csproj" />
<Project Path="src/Aevatar.Studio.Projection/Aevatar.Studio.Projection.csproj" />
</Folder>
<Folder Name="/src/platform/">
<Project Path="src/platform/Aevatar.GAgentService.Governance.Abstractions/Aevatar.GAgentService.Governance.Abstractions.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Aevatar.GAgents.ChatHistory</AssemblyName>
<RootNamespace>Aevatar.GAgents.ChatHistory</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Aevatar.Foundation.Abstractions\Aevatar.Foundation.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Aevatar.Foundation.Core\Aevatar.Foundation.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Tools">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="chat_history_messages.proto" GrpcServices="None" />
</ItemGroup>
</Project>
122 changes: 122 additions & 0 deletions agents/Aevatar.GAgents.ChatHistory/ChatConversationGAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using Aevatar.Foundation.Abstractions;
using Aevatar.Foundation.Abstractions.Attributes;
using Aevatar.Foundation.Core;
using Aevatar.Foundation.Core.EventSourcing;
using Google.Protobuf;
using Microsoft.Extensions.DependencyInjection;

namespace Aevatar.GAgents.ChatHistory;

/// <summary>
/// Per-conversation actor that holds all messages for a single conversation.
/// Actor ID: <c>chat-{scopeId}-{conversationId}</c>.
///
/// When messages are replaced or the conversation is deleted, forwards
/// the change to the <see cref="ChatHistoryIndexGAgent"/> via <c>SendToAsync</c>,
/// ensuring transactional consistency between conversation and index actors.
/// </summary>
public sealed class ChatConversationGAgent : GAgentBase<ChatConversationState>
{
/// <summary>Maximum messages retained per conversation.</summary>
internal const int MaxMessages = 500;

[EventHandler(EndpointName = "replaceMessages")]
public async Task HandleMessagesReplaced(MessagesReplacedEvent evt)
{
if (evt.Meta is null)
return;

// Trim to MaxMessages (keep newest)
var trimmed = TrimMessages(evt);

await PersistDomainEventAsync(trimmed);

// Forward index upsert to the index actor
if (!string.IsNullOrWhiteSpace(evt.ScopeId))
{
var indexActorId = IndexActorId(evt.ScopeId);
await EnsureIndexActorAsync(indexActorId);
var indexMeta = State.Meta?.Clone();
if (indexMeta is not null)
{
indexMeta.MessageCount = State.Messages.Count;
await SendToAsync(indexActorId, new ConversationUpsertedEvent { Meta = indexMeta });
}
}
}

[EventHandler(EndpointName = "deleteConversation")]
public async Task HandleConversationDeleted(ConversationDeletedEvent evt)
{
if (string.IsNullOrWhiteSpace(evt.ConversationId))
return;

// Only delete if there is state to delete
if (State.Meta is null && State.Messages.Count == 0)
return;

await PersistDomainEventAsync(evt);

// Forward index removal to the index actor
if (!string.IsNullOrWhiteSpace(evt.ScopeId))
{
var indexActorId = IndexActorId(evt.ScopeId);
await EnsureIndexActorAsync(indexActorId);
await SendToAsync(indexActorId, new ConversationRemovedEvent { ConversationId = evt.ConversationId });
}
}

protected override async Task OnActivateAsync(CancellationToken ct)
{
await base.OnActivateAsync(ct);
}

protected override ChatConversationState TransitionState(
ChatConversationState current, IMessage evt)
{
return StateTransitionMatcher
.Match(current, evt)
.On<MessagesReplacedEvent>(ApplyMessagesReplaced)
.On<ConversationDeletedEvent>(ApplyConversationDeleted)
.OrCurrent();
}

private static MessagesReplacedEvent TrimMessages(MessagesReplacedEvent evt)
{
if (evt.Messages.Count <= MaxMessages)
return evt;

var trimmed = evt.Clone();
var excess = trimmed.Messages.Count - MaxMessages;
for (var i = 0; i < excess; i++)
trimmed.Messages.RemoveAt(0);

if (trimmed.Meta is not null)
trimmed.Meta.MessageCount = trimmed.Messages.Count;

return trimmed;
}

private static ChatConversationState ApplyMessagesReplaced(
ChatConversationState state, MessagesReplacedEvent evt)
{
var next = new ChatConversationState { Meta = evt.Meta?.Clone() };
next.Messages.AddRange(evt.Messages);
return next;
}

private static ChatConversationState ApplyConversationDeleted(
ChatConversationState state, ConversationDeletedEvent evt)
{
return new ChatConversationState();
}

private async Task EnsureIndexActorAsync(string indexActorId)
{
var runtime = Services.GetRequiredService<IActorRuntime>();
if (await runtime.GetAsync(indexActorId) is null)
await runtime.CreateAsync<ChatHistoryIndexGAgent>(indexActorId);
}

private static string IndexActorId(string scopeId) => $"chat-index-{scopeId}";
}
81 changes: 81 additions & 0 deletions agents/Aevatar.GAgents.ChatHistory/ChatHistoryIndexGAgent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using Aevatar.Foundation.Abstractions;
using Aevatar.Foundation.Abstractions.Attributes;
using Aevatar.Foundation.Core;
using Aevatar.Foundation.Core.EventSourcing;
using Google.Protobuf;

namespace Aevatar.GAgents.ChatHistory;

/// <summary>
/// Per-user index actor that holds conversation list and metadata.
/// Actor ID: <c>chat-index-{scopeId}</c>.
/// </summary>
public sealed class ChatHistoryIndexGAgent : GAgentBase<ChatHistoryIndexState>
{
[EventHandler(EndpointName = "upsertConversation")]
public async Task HandleConversationUpserted(ConversationUpsertedEvent evt)
{
if (evt.Meta is null || string.IsNullOrWhiteSpace(evt.Meta.Id))
return;

await PersistDomainEventAsync(evt);
}

[EventHandler(EndpointName = "removeConversation")]
public async Task HandleConversationRemoved(ConversationRemovedEvent evt)
{
if (string.IsNullOrWhiteSpace(evt.ConversationId))
return;

// Idempotent: skip if not present
var existing = State.Conversations.FirstOrDefault(c =>
string.Equals(c.Id, evt.ConversationId, StringComparison.Ordinal));
if (existing is null)
return;

await PersistDomainEventAsync(evt);
}

protected override async Task OnActivateAsync(CancellationToken ct)
{
await base.OnActivateAsync(ct);
}

protected override ChatHistoryIndexState TransitionState(
ChatHistoryIndexState current, IMessage evt)
{
return StateTransitionMatcher
.Match(current, evt)
.On<ConversationUpsertedEvent>(ApplyConversationUpserted)
.On<ConversationRemovedEvent>(ApplyConversationRemoved)
.OrCurrent();
}

private static ChatHistoryIndexState ApplyConversationUpserted(
ChatHistoryIndexState state, ConversationUpsertedEvent evt)
{
var next = state.Clone();

var existing = next.Conversations.FirstOrDefault(c =>
string.Equals(c.Id, evt.Meta.Id, StringComparison.Ordinal));
if (existing is not null)
next.Conversations.Remove(existing);

next.Conversations.Add(evt.Meta.Clone());
return next;
}

private static ChatHistoryIndexState ApplyConversationRemoved(
ChatHistoryIndexState state, ConversationRemovedEvent evt)
{
var next = state.Clone();

var existing = next.Conversations.FirstOrDefault(c =>
string.Equals(c.Id, evt.ConversationId, StringComparison.Ordinal));
if (existing is not null)
next.Conversations.Remove(existing);

return next;
}

}
64 changes: 64 additions & 0 deletions agents/Aevatar.GAgents.ChatHistory/chat_history_messages.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
syntax = "proto3";
package aevatar.gagents.chathistory;
option csharp_namespace = "Aevatar.GAgents.ChatHistory";

// ─── Shared Types ───

message StoredChatMessageProto {
string id = 1;
string role = 2;
string content = 3;
int64 timestamp = 4;
string status = 5;
string error = 6;
string thinking = 7;
}

message ConversationMetaProto {
string id = 1;
string title = 2;
string service_id = 3;
string service_kind = 4;
int64 created_at_ms = 5;
int64 updated_at_ms = 6;
int32 message_count = 7;
string llm_route = 8;
string llm_model = 9;
}

// ─── ChatConversationGAgent State ───

message ChatConversationState {
repeated StoredChatMessageProto messages = 1;
ConversationMetaProto meta = 2;
}

// ─── ChatConversationGAgent Events ───

message MessagesReplacedEvent {
repeated StoredChatMessageProto messages = 1;
ConversationMetaProto meta = 2;
string scope_id = 3;
}

message ConversationDeletedEvent {
string conversation_id = 1;
string scope_id = 2;
}

// ─── ChatHistoryIndexGAgent State ───

message ChatHistoryIndexState {
repeated ConversationMetaProto conversations = 1;
}

// ─── ChatHistoryIndexGAgent Events ───

message ConversationUpsertedEvent {
ConversationMetaProto meta = 1;
}

message ConversationRemovedEvent {
string conversation_id = 1;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Aevatar.GAgents.ConnectorCatalog</AssemblyName>
<RootNamespace>Aevatar.GAgents.ConnectorCatalog</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Aevatar.Foundation.Abstractions\Aevatar.Foundation.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Aevatar.Foundation.Core\Aevatar.Foundation.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Tools">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="connector_catalog_messages.proto" GrpcServices="None" />
</ItemGroup>
</Project>
Loading
Loading