com.sky.ondeviceagent / Runtime /AgentCore /Memory /JsonlConversationStore.cs
Sky-Kim's picture
Initial commit
2e7837a
Raw
History Blame Contribute Delete
6.2 kB
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
namespace OnDeviceAgent.AgentCore
{
public sealed class JsonlConversationStore : IConversationStore
{
readonly IPersistentPathProvider _paths;
readonly AgentMemoryCompactionPolicy _policy;
readonly string _conversationFileName;
readonly string _summaryFileName;
readonly SemaphoreSlim _fileLock = new SemaphoreSlim(1, 1);
public JsonlConversationStore(
IPersistentPathProvider paths,
AgentMemoryCompactionPolicy compactionPolicy,
string conversationFileName = "conversation.jsonl",
string summaryFileName = "summary.md")
{
_paths = paths ?? throw new ArgumentNullException(nameof(paths));
_policy = compactionPolicy ?? throw new ArgumentNullException(nameof(compactionPolicy));
_conversationFileName = conversationFileName;
_summaryFileName = summaryFileName;
}
public async Task AppendAsync(ConversationTurn turn, CancellationToken ct)
{
if (turn == null) throw new ArgumentNullException(nameof(turn));
ct.ThrowIfCancellationRequested();
await _fileLock.WaitAsync(ct).ConfigureAwait(false);
try
{
await Task.Run(() =>
{
EnsureDirectory();
var line = JsonConvert.SerializeObject(turn);
File.AppendAllText(GetLogPath(), line + Environment.NewLine, Encoding.UTF8);
var turns = LoadTurnsFromDisk();
if (!_policy.ShouldCompact(turns))
return;
var recentCount = Mathf.Max(1, _policy.RecentTurnCount);
var olderCount = turns.Count - recentCount;
if (olderCount > 0)
CompactToDisk(turns, olderCount);
}, ct).ConfigureAwait(false);
}
finally
{
_fileLock.Release();
}
}
public async Task<string> BuildSummaryAsync(CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
await _fileLock.WaitAsync(ct).ConfigureAwait(false);
try
{
return await Task.Run(() =>
{
var summary = LoadSummaryFromDisk();
var turns = LoadTurnsFromDisk();
if (string.IsNullOrWhiteSpace(summary) && turns.Count == 0)
return string.Empty;
return _policy.BuildContext(summary, turns);
}, ct).ConfigureAwait(false);
}
finally
{
_fileLock.Release();
}
}
public async Task CompactNowAsync(CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
await _fileLock.WaitAsync(ct).ConfigureAwait(false);
try
{
await Task.Run(() =>
{
var turns = LoadTurnsFromDisk();
var recentCount = Mathf.Max(1, _policy.RecentTurnCount);
var olderCount = turns.Count - recentCount;
if (olderCount > 0)
CompactToDisk(turns, olderCount);
}, ct).ConfigureAwait(false);
}
finally
{
_fileLock.Release();
}
}
public void ResetSession()
{
_fileLock.Wait();
try
{
DeleteIfExists(GetLogPath());
DeleteIfExists(GetSummaryPath());
}
finally
{
_fileLock.Release();
}
}
string GetLogPath() => Path.Combine(_paths.GetPersistentDataPath(), _conversationFileName);
string GetSummaryPath() => Path.Combine(_paths.GetPersistentDataPath(), _summaryFileName);
void EnsureDirectory()
{
var dir = _paths.GetPersistentDataPath();
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
}
void CompactToDisk(List<ConversationTurn> turns, int olderCount)
{
var olderTurns = turns.GetRange(0, olderCount);
File.WriteAllText(GetSummaryPath(), _policy.BuildSummary(olderTurns), Encoding.UTF8);
var recentTurns = turns.GetRange(olderCount, turns.Count - olderCount);
var sb = new StringBuilder(recentTurns.Count * 256);
for (var i = 0; i < recentTurns.Count; i++)
sb.Append(JsonConvert.SerializeObject(recentTurns[i])).Append(Environment.NewLine);
File.WriteAllText(GetLogPath(), sb.ToString(), Encoding.UTF8);
}
List<ConversationTurn> LoadTurnsFromDisk()
{
var turns = new List<ConversationTurn>();
var path = GetLogPath();
if (!File.Exists(path))
return turns;
foreach (var line in File.ReadAllLines(path))
{
if (string.IsNullOrWhiteSpace(line))
continue;
try
{
var turn = JsonConvert.DeserializeObject<ConversationTurn>(line);
if (turn != null)
turns.Add(turn);
}
catch (Exception ex)
{
Debug.LogWarning("[JsonlConversationStore] Skipped invalid memory line: " + ex.Message);
}
}
return turns;
}
string LoadSummaryFromDisk()
{
var path = GetSummaryPath();
return File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : string.Empty;
}
static void DeleteIfExists(string path)
{
if (File.Exists(path))
File.Delete(path);
}
}
}