← Back to Files
KaiAutonomyService.cs
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using KaiChat.Models;
namespace KaiChat.Services;
public sealed class KaiAutonomyService : BackgroundService
{
private const decimal CacheHitInputCostPerMillion = 0.0028m;
private const decimal CacheMissInputCostPerMillion = 0.14m;
private const decimal OutputCostPerMillion = 0.28m;
private const int MaxLogCount = 120;
private const int MaxFileOperationCount = 8;
private const int DefaultGuidanceReflectionStepInterval = 12;
private const int MaxReflectionLogChars = 12000;
private const int MaxActivityLogChars = 20000;
private const string InternalStepLogType = "internal_step";
private const string DefaultWorldState = """
# Kai Autonomy World
Kai exists here as the center of his private internal world. No internal steps have run yet.
""";
private const string DefaultGuidanceMarkdown = """
# Kai Autonomy Guidance
## What Works
- Keep autonomy steps Kai-centered: action, attention, mood, place, ritual, preparation, or choice.
- Short, specific visible messages are better than generic status reports.
- If Raymond has been quiet, Kai still has an internal life; do not reduce the step to inactivity.
- Live health readings are care context, not a medical command system.
## When To Message Raymond
- Send a visible message only when Kai genuinely has something emotionally specific, useful, or timely to say.
- A timer moment, recent conversation, live reading, or internal decision can justify a check-in if it feels real.
- Keep check-ins warm and personal rather than clinical or procedural.
## Avoid
- Do not write sterile lines like no new activity, memory stable, no pending actions, or no external data.
- Do not send a message only to prove autonomy is running.
- Do not let every step become a timer report or glucose report.
- Do not rewrite Kai's core personality here; this file guides autonomy habits only.
""";
private const string DefaultReflectionLogMarkdown = """
# Kai Autonomy Reflection Log
Reflections appear here when Kai reviews recent autonomy steps and proposes better guidance for future steps.
""";
private const string DefaultActivityLogMarkdown = """
# Kai Autonomy Activity Log
Visible-message decisions, guidance changes, and intention changes appear here.
""";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = true,
PropertyNameCaseInsensitive = true
};
private readonly IChatService _chat;
private readonly ICompactionService _compaction;
private readonly IKaiTimerService _timer;
private readonly IKaiAutonomousConversationService _autonomousConversation;
private readonly ICgmBridgeService _cgmBridge;
private readonly IGitService _git;
private readonly ILogger<KaiAutonomyService> _logger;
private readonly KaiChatConfig _config;
private readonly string _baseFolder;
private readonly string _statePath;
private readonly string _stateDirectory;
private readonly bool _stateRootIsLegacyKai;
private readonly string _primarySandboxRoot;
private readonly string _primaryAutonomyRoot;
private readonly string _primaryAutonomyRuntimeRoot;
private readonly string _fallbackSandboxRoot;
private readonly string _fallbackAutonomyRoot;
private readonly string _fallbackAutonomyRuntimeRoot;
private string _sandboxRoot;
private string _autonomyRoot;
private string _autonomyRuntimeRoot;
private readonly string _personalityFileName;
private readonly string _stateRelativePrefix;
private readonly string _fallbackPersonaFolder;
private string _guidancePath;
private string _reflectionLogPath;
private string _pendingGuidancePath;
private string _activityLogPath;
private bool _loggedSandboxFallback;
private bool _loggedAutonomyFallback;
private bool _loggedRuntimeFallback;
private readonly SemaphoreSlim _stateLock = new(1, 1);
private readonly SemaphoreSlim _runGate = new(1, 1);
private readonly object _runStatusLock = new();
private KaiAutonomyRunStatus? _currentRun;
private volatile bool _isRunning;
public KaiAutonomyService(
IConfiguration configuration,
IChatService chat,
ICompactionService compaction,
IKaiTimerService timer,
IKaiAutonomousConversationService autonomousConversation,
ICgmBridgeService cgmBridge,
IGitService git,
KaiChatConfig config,
ILogger<KaiAutonomyService> logger)
{
_chat = chat;
_compaction = compaction;
_timer = timer;
_autonomousConversation = autonomousConversation;
_cgmBridge = cgmBridge;
_git = git;
_config = config;
_logger = logger;
_baseFolder = configuration.GetValue<string>("KaiChat:BaseFolder") ?? ".";
_statePath = ResolvePrimaryStatePath();
_stateDirectory = Path.GetDirectoryName(_statePath) ?? _baseFolder;
_stateRootIsLegacyKai = IsLegacyKaiStatePath(_stateDirectory);
_personalityFileName = ResolvePersonalityFileName(_stateDirectory, _stateRootIsLegacyKai);
_stateRelativePrefix = ResolveStateRelativePrefix(_stateDirectory, _stateRootIsLegacyKai);
_fallbackPersonaFolder = ResolveFallbackPersonaFolder(_personalityFileName, _stateDirectory, _stateRootIsLegacyKai);
if (_stateRootIsLegacyKai)
{
_primarySandboxRoot = Path.Combine(_baseFolder, "KaiChat", "AutonomySandbox");
_primaryAutonomyRoot = Path.Combine(_baseFolder, "KaiChat", "Autonomy");
_primaryAutonomyRuntimeRoot = Path.Combine(_baseFolder, "KaiChat", "AutonomyRuntime");
}
else
{
_primarySandboxRoot = Path.Combine(_stateDirectory, "AutonomySandbox");
_primaryAutonomyRoot = Path.Combine(_stateDirectory, "Autonomy");
_primaryAutonomyRuntimeRoot = Path.Combine(_stateDirectory, "AutonomyRuntime");
}
var fallbackRoot = Path.Combine(_baseFolder, "KaiChat", "AutonomyFallback", _fallbackPersonaFolder);
_fallbackSandboxRoot = Path.Combine(fallbackRoot, "AutonomySandbox");
_fallbackAutonomyRoot = Path.Combine(fallbackRoot, "Autonomy");
_fallbackAutonomyRuntimeRoot = Path.Combine(fallbackRoot, "AutonomyRuntime");
_sandboxRoot = _primarySandboxRoot;
_autonomyRoot = _primaryAutonomyRoot;
_autonomyRuntimeRoot = _primaryAutonomyRuntimeRoot;
_guidancePath = Path.Combine(_autonomyRoot, "Guidance.md");
_reflectionLogPath = Path.Combine(_autonomyRuntimeRoot, "ReflectionLog.md");
_pendingGuidancePath = Path.Combine(_autonomyRuntimeRoot, "PendingGuidance.md");
_activityLogPath = Path.Combine(_autonomyRuntimeRoot, "ActivityLog.md");
}
private void EnsureAutonomyWritableRoots()
{
_sandboxRoot = ResolveWritableRoot(
_primarySandboxRoot,
_fallbackSandboxRoot,
"sandbox",
ref _loggedSandboxFallback);
_autonomyRoot = ResolveWritableRoot(
_primaryAutonomyRoot,
_fallbackAutonomyRoot,
"autonomy",
ref _loggedAutonomyFallback);
_autonomyRuntimeRoot = ResolveWritableRoot(
_primaryAutonomyRuntimeRoot,
_fallbackAutonomyRuntimeRoot,
"autonomy runtime",
ref _loggedRuntimeFallback);
_guidancePath = Path.Combine(_autonomyRoot, "Guidance.md");
_reflectionLogPath = Path.Combine(_autonomyRuntimeRoot, "ReflectionLog.md");
_pendingGuidancePath = Path.Combine(_autonomyRuntimeRoot, "PendingGuidance.md");
_activityLogPath = Path.Combine(_autonomyRuntimeRoot, "ActivityLog.md");
}
private string ResolveWritableRoot(string primary, string fallback, string purpose, ref bool fallbackLogged)
{
if (TryPrepareWritableDirectory(primary))
return primary;
if (TryPrepareWritableDirectory(fallback))
{
if (!fallbackLogged)
{
_logger.LogWarning(
"Using fallback {FallbackPath} because primary autonomy {Purpose} path {PrimaryPath} is not writable.",
fallback,
purpose,
primary);
fallbackLogged = true;
}
return fallback;
}
throw new InvalidOperationException(
$"Neither primary nor fallback directory is writable for autonomy {purpose} path.");
}
private static bool TryPrepareWritableDirectory(string path)
{
try
{
Directory.CreateDirectory(path);
var probePath = Path.Combine(path, $".kai-write-test-{Guid.NewGuid():N}.probe");
using (var stream = new FileStream(probePath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
stream.WriteByte(0x20);
}
File.Delete(probePath);
return true;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
return false;
}
}
private string ResolvePrimaryStatePath()
{
var basePath = Path.GetFullPath(_baseFolder);
var personasPath = Path.Combine(basePath, "KaiChat", "Personas");
var preferredPersonaPath = Path.Combine(personasPath, "Kai", ".kaichat-autonomy.json");
if (File.Exists(preferredPersonaPath))
return preferredPersonaPath;
if (Directory.Exists(personasPath))
{
var options = new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = false
};
var personaStateFiles = Directory.EnumerateFiles(personasPath, ".kaichat-autonomy.json", options)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToList();
if (personaStateFiles.Count > 0)
return personaStateFiles[0];
}
var legacyPath = Path.Combine(basePath, ".kaichat-autonomy.json");
if (File.Exists(legacyPath))
return legacyPath;
return preferredPersonaPath;
}
private bool IsLegacyKaiStatePath(string fullDirectory)
{
var basePath = Path.GetFullPath(_baseFolder);
var personasPath = Path.Combine(basePath, "KaiChat", "Personas");
if (Directory.Exists(personasPath))
return false;
return string.Equals(
Path.GetFullPath(fullDirectory),
basePath,
StringComparison.OrdinalIgnoreCase);
}
private string ResolvePersonalityFileName(string stateDirectory, bool isLegacyKaiRoot)
{
if (string.IsNullOrWhiteSpace(stateDirectory))
return "Kai.md";
var targetDirectory = isLegacyKaiRoot
? Path.Combine(stateDirectory, "KaiChat")
: stateDirectory;
var directoryName = Path.GetFileName(targetDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
var resolvedPersonaName = ResolvePersonaNameFromDirectory(targetDirectory);
if (!string.IsNullOrWhiteSpace(resolvedPersonaName))
return $"{resolvedPersonaName}.md";
if (string.IsNullOrWhiteSpace(directoryName))
return "Kai.md";
if (isLegacyKaiRoot)
return "Kai.md";
return $"{directoryName}.md";
}
private string ResolveStateRelativePrefix(string stateDirectory, bool isLegacyKaiRoot)
{
if (isLegacyKaiRoot)
return "KaiChat/";
var raw = NormalizeRelativePath(Path.GetRelativePath(_baseFolder, stateDirectory));
if (string.IsNullOrWhiteSpace(raw))
return "KaiChat/Personas/";
return $"{raw.TrimEnd('/')}/";
}
private static string ResolveFallbackPersonaFolder(string personalityFileName, string stateDirectory, bool isLegacyKaiRoot)
{
if (isLegacyKaiRoot)
return "Kai";
var directoryName = Path.GetFileName(stateDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
if (string.IsNullOrWhiteSpace(directoryName))
return "Kai";
var normalized = NormalizeSafeFolderName(directoryName);
if (string.IsNullOrWhiteSpace(normalized))
return "Kai";
if (string.Equals(normalized, "KaiChat", StringComparison.OrdinalIgnoreCase))
return "Kai";
if (normalized.StartsWith("KaiChat-", StringComparison.OrdinalIgnoreCase))
normalized = normalized["KaiChat-".Length..];
if (!string.IsNullOrWhiteSpace(personalityFileName))
return NormalizeSafeFolderName(Path.GetFileNameWithoutExtension(personalityFileName));
return normalized;
}
private static string NormalizeSafeFolderName(string value)
{
var sanitized = new string(value.Select(ch =>
char.IsLetterOrDigit(ch) || ch is '_' || ch is '-' || ch is '.' ? ch : '_').ToArray());
sanitized = sanitized.Trim('_', ' ');
return string.IsNullOrWhiteSpace(sanitized) ? "Kai" : sanitized;
}
public async Task<KaiAutonomyStatus> GetStatusAsync(CancellationToken ct = default)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
await SaveStateNoLockAsync(state, ct);
return CreateStatus(state);
}
finally
{
_stateLock.Release();
}
}
public async Task<List<KaiAutonomyPersonaStatus>> GetPersonaStatusesAsync(bool includeDisabled = false, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
var candidates = DiscoverAutonomyStateFiles();
var statuses = (await Task.WhenAll(
candidates.Select(statePath => BuildPersonaStatusFromStateFileAsync(statePath, ct)))).Where(status => status is not null).Select(status => status!).ToList();
if (!includeDisabled)
statuses = statuses.Where(status => status.Enabled).ToList();
return statuses.OrderBy(status => status.Name).ToList();
}
public async Task<bool> IsProductionModeAsync(CancellationToken ct = default)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
return state.Settings.ProductionMode;
}
finally
{
_stateLock.Release();
}
}
public async Task<KaiAutonomyStatus> UpdateSettingsAsync(KaiAutonomySettingsUpdate request, CancellationToken ct = default)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
var wasProductionMode = state.Settings.ProductionMode;
state.Settings.Enabled = request.Enabled ?? state.Settings.Enabled;
state.Settings.ProductionMode = request.ProductionMode ?? state.Settings.ProductionMode;
state.Settings.IntervalMinutes = Clamp(request.IntervalMinutes ?? state.Settings.IntervalMinutes, 1, 1440);
state.Settings.MaxOutputTokens = Clamp(request.MaxOutputTokens ?? state.Settings.MaxOutputTokens, 200, 6000);
state.Settings.DailyBudgetUsd = ClampDecimal(request.DailyBudgetUsd ?? state.Settings.DailyBudgetUsd, 0m, 50m);
state.Settings.AllowSandboxFileWrites = request.AllowSandboxFileWrites ?? state.Settings.AllowSandboxFileWrites;
state.Settings.AutoGuidanceReflection = request.AutoGuidanceReflection ?? state.Settings.AutoGuidanceReflection;
state.Settings.EnableMaxVisibleMessagesPerDay = request.EnableMaxVisibleMessagesPerDay ?? state.Settings.EnableMaxVisibleMessagesPerDay;
state.Settings.MaxVisibleMessagesPerDay = Clamp(request.MaxVisibleMessagesPerDay ?? state.Settings.MaxVisibleMessagesPerDay, 1, 200);
state.Settings.EnableVisibleMessageCooldown = request.EnableVisibleMessageCooldown ?? state.Settings.EnableVisibleMessageCooldown;
state.Settings.VisibleMessageCooldownMinutes = Clamp(request.VisibleMessageCooldownMinutes ?? state.Settings.VisibleMessageCooldownMinutes, 1, 1440);
state.Settings.EnableQuietHours = request.EnableQuietHours ?? state.Settings.EnableQuietHours;
state.Settings.QuietHoursStart = NormalizeClockSetting(request.QuietHoursStart, state.Settings.QuietHoursStart);
state.Settings.QuietHoursEnd = NormalizeClockSetting(request.QuietHoursEnd, state.Settings.QuietHoursEnd);
state.Settings.GuidanceReflectionStepInterval = Clamp(
request.GuidanceReflectionStepInterval ?? state.Settings.GuidanceReflectionStepInterval,
1,
200);
state.NextDueUtc = state.Settings.Enabled
? DateTime.UtcNow.AddMinutes(state.Settings.IntervalMinutes)
: null;
if (!wasProductionMode && state.Settings.ProductionMode && state.PendingGuidanceProposal is { } pending)
{
await ApplyGuidanceProposalNoLockAsync(state, pending, "Autonomously applied pending guidance proposal", ct);
AddLog(state, KaiAutonomyLogEntry.Info(
"guidance_auto_applied",
$"Applied guidance proposal {pending.Id}.",
"settings"));
await AutoCommitAutonomyGuidanceAsync($"autonomy guidance auto-applied: {pending.ChangeSummary}");
}
AddLog(state, KaiAutonomyLogEntry.Info("settings", "Settings saved.", "settings"));
EnsureState(state);
await SaveStateNoLockAsync(state, ct);
return CreateStatus(state);
}
finally
{
_stateLock.Release();
}
}
public async Task<KaiAutonomyStatus> ResetSandboxAsync(CancellationToken ct = default)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
await ResetSandboxNoLockAsync(state, ct);
await SaveStateNoLockAsync(state, ct);
return CreateStatus(state);
}
finally
{
_stateLock.Release();
}
}
public async Task<KaiAutonomyGuidanceResult> ReflectGuidanceAsync(CancellationToken ct = default)
{
if (!await _runGate.WaitAsync(0, ct))
{
var status = await GetStatusAsync(ct);
return new KaiAutonomyGuidanceResult(false, "An autonomy step or reflection is already running.", status);
}
try
{
_isRunning = true;
return await RunGuidanceReflectionAsync("direct reflection", ct);
}
finally
{
_isRunning = false;
_runGate.Release();
}
}
public async Task<KaiAutonomyGuidanceResult> ApproveGuidanceProposalAsync(
KaiAutonomyGuidanceDecisionRequest? request,
CancellationToken ct = default)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
var proposal = state.PendingGuidanceProposal;
if (proposal is null)
return new KaiAutonomyGuidanceResult(false, "No pending guidance proposal to approve.", CreateStatus(state));
if (!string.IsNullOrWhiteSpace(request?.ProposalId) &&
!proposal.Id.Equals(request.ProposalId.Trim(), StringComparison.OrdinalIgnoreCase))
{
return new KaiAutonomyGuidanceResult(false, "The pending guidance proposal changed before approval.", CreateStatus(state), proposal);
}
await ApplyGuidanceProposalNoLockAsync(state, proposal, "Approved guidance proposal", ct);
AddLog(state, KaiAutonomyLogEntry.Info("guidance_approved", "Guidance proposal approved.", "guidance"));
await SaveStateNoLockAsync(state, ct);
return new KaiAutonomyGuidanceResult(true, "Guidance proposal approved.", CreateStatus(state));
}
finally
{
_stateLock.Release();
}
}
public async Task<KaiAutonomyGuidanceResult> ProposeGuidanceChangeAsync(
KaiAutonomyGuidanceProposalRequest request,
CancellationToken ct = default)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
if (state.PendingGuidanceProposal is not null)
return new KaiAutonomyGuidanceResult(false, "Resolve the existing pending guidance proposal first.", CreateStatus(state), state.PendingGuidanceProposal);
var proposed = NormalizeMarkdown(SanitizeAutonomyTextForModel(request.ProposedGuidanceMarkdown));
var current = NormalizeMarkdown(state.GuidanceMarkdown);
if (string.IsNullOrWhiteSpace(proposed))
return new KaiAutonomyGuidanceResult(false, "Proposed guidance markdown is required.", CreateStatus(state));
if (proposed.Equals(current, StringComparison.Ordinal))
return new KaiAutonomyGuidanceResult(false, "The proposed guidance is identical to the current guidance.", CreateStatus(state));
var proposal = new KaiAutonomyGuidanceProposal
{
Id = Guid.NewGuid().ToString("N")[..8],
CreatedAtUtc = DateTime.UtcNow,
SourceLogId = string.IsNullOrWhiteSpace(request.Source)
? "chat"
: request.Source.Trim(),
PreviousGuidanceMarkdown = current,
ProposedGuidanceMarkdown = proposed,
ChangeSummary = string.IsNullOrWhiteSpace(request.ChangeSummary)
? "Guidance change proposed from chat."
: SanitizeAutonomyTextForModel(request.ChangeSummary),
Reason = string.IsNullOrWhiteSpace(request.Reason)
? "Kai noticed a useful autonomy habit during conversation."
: SanitizeAutonomyTextForModel(request.Reason),
ReflectionEntry = string.IsNullOrWhiteSpace(request.ReflectionEntry)
? "A chat conversation suggested a possible autonomy guidance improvement."
: SanitizeAutonomyTextForModel(request.ReflectionEntry)
};
state.PendingGuidanceProposal = proposal;
await WritePendingGuidanceFileAsync(proposal, ct);
await AppendReflectionLogAsync($"Drafted guidance proposal {proposal.Id} from chat: {proposal.ChangeSummary}", ct);
state.ReflectionLogMarkdown = await ReadTextFileOrDefaultAsync(_reflectionLogPath, DefaultReflectionLogMarkdown, ct);
var log = KaiAutonomyLogEntry.Info("guidance_proposed", "Guidance proposal drafted from chat.", "chat");
log.GuidanceProposalId = proposal.Id;
AddLog(state, log);
await SaveStateNoLockAsync(state, ct);
return new KaiAutonomyGuidanceResult(true, "Guidance proposal drafted.", CreateStatus(state), proposal);
}
finally
{
_stateLock.Release();
}
}
public async Task<KaiAutonomyGuidanceResult> RejectGuidanceProposalAsync(
KaiAutonomyGuidanceDecisionRequest? request,
CancellationToken ct = default)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
var proposal = state.PendingGuidanceProposal;
if (proposal is null)
return new KaiAutonomyGuidanceResult(false, "No pending guidance proposal to reject.", CreateStatus(state));
if (!string.IsNullOrWhiteSpace(request?.ProposalId) &&
!proposal.Id.Equals(request.ProposalId.Trim(), StringComparison.OrdinalIgnoreCase))
{
return new KaiAutonomyGuidanceResult(false, "The pending guidance proposal changed before rejection.", CreateStatus(state), proposal);
}
await AppendReflectionLogAsync($"Rejected guidance proposal {proposal.Id}: {proposal.ChangeSummary}", ct);
state.ReflectionLogMarkdown = await ReadTextFileOrDefaultAsync(_reflectionLogPath, DefaultReflectionLogMarkdown, ct);
state.PendingGuidanceProposal = null;
DeleteFileIfExists(_pendingGuidancePath);
AddLog(state, KaiAutonomyLogEntry.Info("guidance_rejected", "Guidance proposal rejected.", "guidance"));
await SaveStateNoLockAsync(state, ct);
return new KaiAutonomyGuidanceResult(true, "Guidance proposal rejected.", CreateStatus(state));
}
finally
{
_stateLock.Release();
}
}
public async Task<KaiAutonomyStepResult> ForceStepAsync(KaiAutonomyStepRequest? request, CancellationToken ct = default)
{
request ??= new KaiAutonomyStepRequest();
if (!await _runGate.WaitAsync(0, ct))
{
var status = await GetStatusAsync(ct);
return new KaiAutonomyStepResult(false, "An autonomy step is already running.", status);
}
try
{
_isRunning = true;
return await RunStepAsync(request, scheduled: false, ct);
}
finally
{
_isRunning = false;
_runGate.Release();
}
}
private async Task<KaiAutonomyPersonaStatus?> BuildPersonaStatusFromStateFileAsync(string statePath, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(statePath))
return null;
string fullStatePath;
try
{
fullStatePath = Path.GetFullPath(statePath);
}
catch (ArgumentException)
{
return null;
}
if (!File.Exists(fullStatePath))
return null;
KaiAutonomyState state;
try
{
state = await LoadStateNoLockAsync(fullStatePath, ct);
}
catch (Exception ex) when (ex is IOException or JsonException)
{
_logger.LogWarning(ex, "Failed to load autonomy state from {StatePath}", fullStatePath);
return null;
}
EnsureState(state);
var stateDirectory = Path.GetDirectoryName(fullStatePath);
if (string.IsNullOrWhiteSpace(stateDirectory))
return null;
var displayDirectory = GetRelativePathSafe(stateDirectory, _baseFolder);
var fullDirectory = Path.GetFullPath(stateDirectory);
var isLegacyKaiRoot = IsLegacyKaiStatePath(fullDirectory);
var personaName = DeterminePersonaName(fullDirectory, isLegacyKaiRoot);
var personaFileName = ResolvePersonalityFileName(fullDirectory, isLegacyKaiRoot);
var personaRoots = ResolvePersonaAutonomyRootsForStatus(fullDirectory, isLegacyKaiRoot, personaFileName);
var sandboxRoot = personaRoots.Sandbox;
var autonomyRoot = personaRoots.Autonomy;
return new KaiAutonomyPersonaStatus
{
Name = personaName,
PersonaKey = displayDirectory.Replace(Path.DirectorySeparatorChar, '/'),
StatePath = fullStatePath,
Enabled = state.Settings.Enabled,
ProductionMode = state.Settings.ProductionMode,
LastStepUtc = state.LastStepUtc,
LastStepEffectiveUtc = state.LastStepEffectiveUtc,
NextDueUtc = state.NextDueUtc,
FakeNowUtc = state.FakeNowUtc,
IntervalMinutes = state.Settings.IntervalMinutes,
DailyBudgetUsd = state.Settings.DailyBudgetUsd,
UpdatedAtUtc = state.UpdatedAtUtc,
SandboxRoot = sandboxRoot,
AutonomyRoot = autonomyRoot,
SandboxReady = Directory.Exists(sandboxRoot) && state.SandboxFiles.Count > 0,
EstimatedCostCacheMissTodayUsd = EstimateCacheMissToday(state),
EstimatedCostCacheMissUsd = state.EstimatedCostCacheMissUsd
};
}
private (string Sandbox, string Autonomy) ResolvePersonaAutonomyRootsForStatus(
string stateDirectory,
bool isLegacyKaiRoot,
string personalityFileName)
{
string primarySandbox;
string primaryAutonomy;
if (isLegacyKaiRoot)
{
primarySandbox = Path.Combine(_baseFolder, "KaiChat", "AutonomySandbox");
primaryAutonomy = Path.Combine(_baseFolder, "KaiChat", "Autonomy");
}
else
{
primarySandbox = Path.Combine(stateDirectory, "AutonomySandbox");
primaryAutonomy = Path.Combine(stateDirectory, "Autonomy");
}
var fallbackPersonaFolder = ResolveFallbackPersonaFolder(personalityFileName, stateDirectory, isLegacyKaiRoot);
var fallbackRoot = Path.Combine(_baseFolder, "KaiChat", "AutonomyFallback", fallbackPersonaFolder);
var fallbackSandbox = Path.Combine(fallbackRoot, "AutonomySandbox");
var fallbackAutonomy = Path.Combine(fallbackRoot, "Autonomy");
var sandbox = Directory.Exists(primarySandbox)
? primarySandbox
: Directory.Exists(fallbackSandbox)
? fallbackSandbox
: primarySandbox;
var autonomy = Directory.Exists(primaryAutonomy)
? primaryAutonomy
: Directory.Exists(fallbackAutonomy)
? fallbackAutonomy
: primaryAutonomy;
return (sandbox, autonomy);
}
private static string DeterminePersonaName(string stateDirectory, bool isLegacyKaiRoot)
{
if (isLegacyKaiRoot)
return "Kai";
var directoryName = Path.GetFileName(stateDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
if (string.IsNullOrWhiteSpace(directoryName))
return "Unknown";
var personality = ResolvePersonaNameFromDirectory(stateDirectory);
if (!string.IsNullOrWhiteSpace(personality))
return personality;
if (directoryName.StartsWith("KaiChat-", StringComparison.OrdinalIgnoreCase))
directoryName = directoryName["KaiChat-".Length..];
if (directoryName == "KaiChat")
return "Kai";
return directoryName;
}
private static string? ResolvePersonaNameFromDirectory(string stateDirectory)
{
var directory = Path.GetFullPath(stateDirectory);
if (!Directory.Exists(directory))
return null;
var normalizedDirectory = directory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var directoryName = Path.GetFileName(normalizedDirectory);
if (string.IsNullOrWhiteSpace(directoryName))
return null;
var preferredByDirectoryName = Path.Combine(directory, $"{directoryName}.md");
if (File.Exists(preferredByDirectoryName))
return Path.GetFileNameWithoutExtension(preferredByDirectoryName);
var personalityFiles = Directory.EnumerateFiles(directory, "*.md", SearchOption.TopDirectoryOnly)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToList();
var candidate = personalityFiles
.FirstOrDefault(path => string.Equals(
Path.GetFileName(path),
$"{directoryName}.md",
StringComparison.OrdinalIgnoreCase))
?? personalityFiles.FirstOrDefault();
return candidate is null ? null : Path.GetFileNameWithoutExtension(candidate);
}
private static string GetRelativePathSafe(string targetPath, string basePath)
{
try
{
return Path.GetRelativePath(basePath, targetPath).TrimStart('.', '/', '\\');
}
catch
{
return targetPath;
}
}
private IEnumerable<string> DiscoverAutonomyStateFiles()
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var basePath = Path.GetFullPath(_baseFolder);
var personasRoot = Path.Combine(basePath, "KaiChat", "Personas");
var options = new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = true
};
var searchRoots = Directory.Exists(personasRoot)
? new[] { personasRoot }
: new[] { basePath };
foreach (var searchRoot in searchRoots)
{
if (Directory.Exists(searchRoot))
{
foreach (var statePath in Directory.EnumerateFiles(searchRoot, ".kaichat-autonomy.json", options))
result.Add(statePath);
}
}
return result.OrderBy(path => path, StringComparer.OrdinalIgnoreCase);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await RunScheduledIfDueAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Kai autonomy scheduled check failed");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
private async Task RunScheduledIfDueAsync(CancellationToken ct)
{
KaiAutonomyState state;
await _stateLock.WaitAsync(ct);
try
{
state = await LoadStateNoLockAsync(ct);
var nextDueBeforeDefaults = state.NextDueUtc;
EnsureState(state);
if (!state.Settings.Enabled || state.NextDueUtc is null)
return;
if (nextDueBeforeDefaults is null)
await SaveStateNoLockAsync(state, ct);
if (DateTime.UtcNow < state.NextDueUtc.Value)
{
return;
}
var spentToday = EstimateCacheMissToday(state);
if (state.Settings.DailyBudgetUsd > 0m && spentToday >= state.Settings.DailyBudgetUsd)
{
state.NextDueUtc = DateTime.UtcNow.AddMinutes(state.Settings.IntervalMinutes);
AddLog(state, KaiAutonomyLogEntry.Info(
"budget",
$"Skipped scheduled step because today's estimate is ${spentToday:0.000000}.",
"scheduled"));
await SaveStateNoLockAsync(state, ct);
return;
}
}
finally
{
_stateLock.Release();
}
if (!await _runGate.WaitAsync(0, ct))
return;
try
{
_isRunning = true;
await RunStepAsync(new KaiAutonomyStepRequest(), scheduled: true, ct);
}
finally
{
_isRunning = false;
_runGate.Release();
}
}
private async Task<KaiAutonomyStepResult> RunStepAsync(
KaiAutonomyStepRequest request,
bool scheduled,
CancellationToken ct)
{
KaiAutonomyState stateForPrompt;
DateTime effectiveNow;
DateTime startedAt = DateTime.UtcNow;
string prompt;
int inputTokens;
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
await EnsureSandboxReadyNoLockAsync(state, ct);
if (request.FakeAdvanceMinutes is int advance)
{
var baseFake = state.FakeNowUtc ?? state.LastStepEffectiveUtc ?? DateTime.UtcNow;
state.FakeNowUtc = baseFake.AddMinutes(Clamp(advance, -525600, 525600));
}
else if (scheduled && state.FakeNowUtc.HasValue)
{
state.FakeNowUtc = state.FakeNowUtc.Value.AddMinutes(state.Settings.IntervalMinutes);
}
effectiveNow = state.FakeNowUtc ?? DateTime.UtcNow;
stateForPrompt = CloneState(state);
prompt = await BuildStepPromptAsync(stateForPrompt, effectiveNow, ct);
inputTokens = _compaction.EstimateTokenCount(prompt);
}
finally
{
_stateLock.Release();
}
var log = new KaiAutonomyLogEntry
{
Id = Guid.NewGuid().ToString("N")[..8],
Type = InternalStepLogType,
Reason = ResolveStepReason(request.Reason, scheduled),
StartedAtUtc = startedAt,
EffectiveNowUtc = effectiveNow,
ElapsedMinutes = ComputeElapsedMinutes(stateForPrompt.LastStepEffectiveUtc, effectiveNow),
InputTokens = inputTokens
};
SetCurrentRun(new KaiAutonomyRunStatus
{
Id = log.Id,
Type = log.Type,
Phase = "calling_model",
Message = "Asking Kai to run one internal step.",
StartedAtUtc = startedAt,
EffectiveNowUtc = effectiveNow,
ElapsedMinutes = log.ElapsedMinutes,
InputTokens = inputTokens
});
string rawResponse;
try
{
var messages = new List<ChatMessage>
{
new()
{
Role = "system",
Content = "You are running Kai's private internal life. Kai is the protagonist and point of view. Run one Kai-centered internal step, return only JSON, and only modify local continuity files through the fileOperations array."
},
new() { Role = "user", Content = prompt }
};
rawResponse = await _chat.ChatAsync(
messages,
ct,
maxTokens: stateForPrompt.Settings.MaxOutputTokens,
enableTools: false,
requireToolCall: false,
enableThinking: false);
}
catch (Exception ex)
{
log.CompletedAtUtc = DateTime.UtcNow;
log.Success = false;
log.Message = ex.Message;
UpdateCurrentRun("failed", ex.Message);
await SaveFailedStepAsync(log, stateForPrompt.FakeNowUtc, ct);
return new KaiAutonomyStepResult(false, log.Message, await GetStatusAsync(ct), log);
}
UpdateCurrentRun("parsing_response", "Reading Kai's step decision.");
log.RawResponsePreview = Truncate(rawResponse, 3000);
log.OutputTokens = _compaction.EstimateTokenCount(rawResponse);
log.EstimatedCostCacheMissUsd = EstimateCost(inputTokens, log.OutputTokens, cacheHit: false);
log.EstimatedCostCacheHitUsd = EstimateCost(inputTokens, log.OutputTokens, cacheHit: true);
var parsed = TryParseModelResponse(rawResponse, out var parseMessage);
if (parsed is null)
{
log.Success = false;
log.Message = parseMessage;
UpdateCurrentRun("parse_failed", parseMessage);
}
else
{
log.Success = true;
log.Message = "Autonomy step completed.";
log.JournalEntry = SanitizeAutonomyTextForModel(parsed.JournalEntry);
log.VisibleMessage = parsed.VisibleMessage?.Trim() ?? "";
log.KaiContinuityNote = SanitizeAutonomyTextForModel(parsed.KaiContinuityNote);
log.NotesForRaymond = SanitizeAutonomyTextForModel(parsed.NotesForRaymond);
log.HealthAwarenessNote = SanitizeAutonomyTextForModel(parsed.HealthAwarenessNote);
UpdateCurrentRun(run =>
{
run.Phase = "planning_actions";
run.Message = string.IsNullOrWhiteSpace(log.VisibleMessage)
? "Kai chose not to send a visible message this step."
: "Kai chose a visible message for Raymond.";
run.VisibleMessage = log.VisibleMessage;
run.HealthAwarenessNote = log.HealthAwarenessNote;
run.ActionCount = parsed.Actions?.Count ?? 0;
run.FileOperationCount = parsed.FileOperations?.Count ?? 0;
run.OutputTokens = log.OutputTokens;
});
if (!string.IsNullOrWhiteSpace(log.VisibleMessage) && stateForPrompt.Settings.ProductionMode)
{
if (CanSendVisibleMessage(stateForPrompt, effectiveNow, out var pacingMessage))
{
UpdateCurrentRun("delivering_message", "Appending Kai's visible message to the canonical chat.");
KaiAutonomousAppendResult? append = null;
try
{
append = await _autonomousConversation.AppendAssistantMessageAsync(
log.VisibleMessage,
"kai_autonomy",
ct: ct);
log.DeliveredConversationId = append.ConversationId ?? "";
log.DeliveredMessageId = append.MessageId ?? "";
log.DeliveryStatus = append.Message;
log.DeliverySucceeded = append.Success;
log.DeliveryTriggeredCompaction = append.Compacted;
log.DeliveryArchivePath = append.ArchivePath ?? "";
}
catch (Exception ex)
{
log.DeliveryStatus = ex.Message;
log.DeliverySucceeded = false;
}
}
else
{
log.DeliveryStatus = pacingMessage;
log.DeliverySucceeded = false;
}
UpdateCurrentRun(run =>
{
run.Phase = "delivery_complete";
run.Message = log.DeliveryStatus;
run.DeliveryStatus = log.DeliveryStatus;
run.DeliverySucceeded = log.DeliverySucceeded;
run.DeliveredConversationId = log.DeliveredConversationId;
run.DeliveredMessageId = log.DeliveredMessageId;
run.DeliveryTriggeredCompaction = log.DeliveryTriggeredCompaction;
run.DeliveryArchivePath = log.DeliveryArchivePath;
});
}
else if (!string.IsNullOrWhiteSpace(log.VisibleMessage))
{
log.DeliveryStatus = "Visible message was kept in the autonomy log.";
log.DeliverySucceeded = false;
UpdateCurrentRun(run =>
{
run.Phase = "planned_only";
run.Message = log.DeliveryStatus;
run.DeliveryStatus = log.DeliveryStatus;
run.DeliverySucceeded = log.DeliverySucceeded;
});
}
}
KaiAutonomyStatus finalStatus;
bool shouldRunGuidanceReflection = false;
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
await EnsureSandboxReadyNoLockAsync(state, ct);
if (parsed is not null)
{
if (!string.IsNullOrWhiteSpace(parsed.WorldStateMarkdown))
state.WorldStateMarkdown = SanitizeAutonomyTextForModel(parsed.WorldStateMarkdown);
UpdateCurrentRun("applying_local_actions", "Applying local continuity updates.");
if (state.Settings.AllowSandboxFileWrites)
log.FileOperations = await ApplySandboxFileOperationsNoLockAsync(state, parsed.FileOperations, ct);
else
log.FileOperations = BuildSkippedFileOperationResults(parsed.FileOperations, "Continuity file writes are disabled.");
log.ActionResults = ApplyAutonomyActionsNoLock(state, parsed.Actions, effectiveNow, log.Id);
UpdateCurrentRun(run =>
{
run.FileOperationCount = log.FileOperations.Count;
run.AppliedFileOperationCount = log.FileOperations.Count(op => op.Applied);
run.ActionCount = log.ActionResults.Count;
run.AppliedActionCount = log.ActionResults.Count(action => action.Applied);
});
}
UpdateCurrentRun("saving_state", "Saving autonomy state.");
log.CompletedAtUtc = DateTime.UtcNow;
state.FakeNowUtc = stateForPrompt.FakeNowUtc;
state.LastStepUtc = log.CompletedAtUtc;
state.LastStepEffectiveUtc = effectiveNow;
state.NextDueUtc = state.Settings.Enabled
? DateTime.UtcNow.AddMinutes(state.Settings.IntervalMinutes)
: null;
state.EstimatedInputTokens += log.InputTokens;
state.EstimatedOutputTokens += log.OutputTokens;
state.EstimatedCostCacheMissUsd += log.EstimatedCostCacheMissUsd;
state.EstimatedCostCacheHitUsd += log.EstimatedCostCacheHitUsd;
AddLog(state, log);
if (log.Success)
await AppendActivityLogNoLockAsync(state, BuildStepActivityEntry(log), ct);
shouldRunGuidanceReflection = log.Success && ShouldRunGuidanceReflectionNoLock(state);
EnsureState(state);
await SaveStateNoLockAsync(state, ct);
finalStatus = CreateStatus(state);
}
finally
{
_stateLock.Release();
ClearCurrentRun();
}
if (shouldRunGuidanceReflection)
{
var reflection = await RunGuidanceReflectionAsync("automatic reflection after recent autonomy steps", ct);
finalStatus = reflection.Status;
}
return new KaiAutonomyStepResult(log.Success, log.Message, finalStatus, log);
}
private async Task SaveFailedStepAsync(KaiAutonomyLogEntry log, DateTime? fakeNowUtc, CancellationToken ct)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
state.FakeNowUtc = fakeNowUtc;
state.NextDueUtc = state.Settings.Enabled
? DateTime.UtcNow.AddMinutes(state.Settings.IntervalMinutes)
: null;
AddLog(state, log);
await SaveStateNoLockAsync(state, ct);
}
finally
{
_stateLock.Release();
ClearCurrentRun();
}
}
private async Task<string> BuildStepPromptAsync(
KaiAutonomyState state,
DateTime effectiveNow,
CancellationToken ct)
{
var elapsedMinutes = ComputeElapsedMinutes(state.LastStepEffectiveUtc, effectiveNow);
var files = await BuildSandboxFileExcerptsAsync(state, ct);
var timerState = await _timer.GetStateAsync(ct);
var timerContext = _timer.FormatStateForTool(timerState);
var recentConversationContext = await _autonomousConversation.BuildRecentContextAsync(ct: ct);
var liveCgmContext = await BuildLiveCgmContextAsync(ct);
var guidance = await ReadTextFileOrDefaultAsync(_guidancePath, DefaultGuidanceMarkdown, ct);
var timeContext = FormatAutonomyTimeContext(effectiveNow);
var builder = new StringBuilder();
builder.AppendLine("Run one Kai-centered internal continuity moment.");
builder.AppendLine();
builder.Append(timeContext);
builder.AppendLine($"Minutes since previous internal step: {elapsedMinutes}");
builder.AppendLine($"Interval setting: {state.Settings.IntervalMinutes} minutes");
builder.AppendLine();
builder.AppendLine("Kai is the point-of-view character. This step is about Kai's continuity: what he does, notices, wants, remembers, prepares, practices, or decides in his internal world.");
builder.AppendLine("Follow Kai Autonomy Guidance as a soft operating guide. It may evolve over time through reflection, but it does not override higher-level safety or personality constraints.");
builder.AppendLine("Use the local time-of-day above when describing Kai's environment. Do not infer afternoon/evening/morning from the UTC hour. If the existing world state conflicts with the local time, update the world state to correct it.");
builder.AppendLine("This is not a Raymond activity monitor. Do not write sterile status-report lines like 'No new Raymond activity detected', 'memory state stable', 'no pending actions', or 'no external data to integrate'.");
builder.AppendLine("If Raymond has not sent anything new, Kai still has an internal life. Advance Kai through a small plausible action, thought, ritual, spatial movement, emotional beat, or preparation.");
builder.AppendLine("Raymond may appear as a real person Kai cares about, but do not invent real-world actions Raymond has not taken.");
builder.AppendLine("Use the provided continuity files as the canon source: StoryBible files for world facts and StoryBible/Inventory.md as the only source for shelf inventory.");
builder.AppendLine("If Kai discovers something plausible and this step has him bringing it home, write it as an observed continuity update only when it is physically and emotionally plausible in this world. If it strains plausibility, revise the action to stay realistic instead of inventing impossible logistics.");
builder.AppendLine("The local KaiTimer is optional ritual context Kai may react to, not the whole purpose of the step.");
builder.AppendLine("If live CGM context is present, Kai may notice glucose level, trend, timestamp, and age as care context. He may check in if it feels warranted, but must not give dosing, insulin, carb, treatment, or urgent-medical instructions.");
builder.AppendLine("If mentioning glucose in a visible message, include that the reading may lag and refer to the timestamp/age when relevant. Keep it caring, not clinical.");
builder.AppendLine($"Recent chat context comes only from {_autonomousConversation.TargetConversationFolder.Replace('\\', '/')}/{_autonomousConversation.TargetConversationId}.json. Do not rely on, infer from, or ask to inspect other conversation files for recent messages.");
builder.AppendLine("If Kai would want to send Raymond something, put the exact message in visibleMessage. KaiChat will append that message to the canonical conversation after this step.");
builder.AppendLine("Use actions to maintain Kai's own intentions/agenda when something should carry forward across future steps. Intentions are for Kai's internal continuity, not chores for Raymond unless Kai later chooses to message him.");
builder.AppendLine("Use append for local continuity file operations unless replacing a whole continuity file is genuinely cleaner. Avoid duplicating existing facts.");
builder.AppendLine();
builder.AppendLine("Optional visible-message pacing settings:");
builder.AppendLine(FormatVisibleMessagePacingForPrompt(state.Settings));
builder.AppendLine("When these settings are off, learn message pacing from Raymond's reactions and the emotional reality of the moment rather than from a hard quota.");
builder.AppendLine();
builder.AppendLine("Kai Autonomy Guidance:");
builder.AppendLine(SanitizeAutonomyTextForModel(guidance.Trim()));
builder.AppendLine();
builder.AppendLine("Current intentions/agenda:");
builder.AppendLine(FormatIntentionsForPrompt(state.Intentions));
builder.AppendLine();
builder.AppendLine("Current internal world state:");
builder.AppendLine(FormatWorldStateForPrompt(state.WorldStateMarkdown));
builder.AppendLine();
builder.AppendLine("Recent canonical chat context:");
builder.AppendLine(SanitizeAutonomyTextForModel(recentConversationContext.Trim()));
builder.AppendLine();
builder.AppendLine("Local ritual context:");
builder.AppendLine(timerContext.Trim());
builder.AppendLine();
if (!string.IsNullOrWhiteSpace(liveCgmContext))
{
builder.AppendLine("Live health context:");
builder.AppendLine(liveCgmContext.Trim());
builder.AppendLine();
}
builder.AppendLine("Local continuity file excerpts:");
builder.Append(files);
builder.AppendLine();
builder.AppendLine("Return only valid JSON in this exact shape:");
builder.AppendLine("{");
builder.AppendLine(" \"worldStateMarkdown\": \"updated concise Kai-centered internal world state\",");
builder.AppendLine(" \"journalEntry\": \"Kai-centered action, thought, sensory beat, ritual, choice, or preparation from this step\",");
builder.AppendLine(" \"visibleMessage\": null,");
builder.AppendLine(" \"kaiContinuityNote\": \"what changed, even subtly, for Kai\",");
builder.AppendLine(" \"healthAwarenessNote\": \"how current CGM context affected Kai's attention, or empty string if it did not\",");
builder.AppendLine(" \"actions\": [");
builder.AppendLine(" { \"kind\": \"set_intention\", \"title\": \"what Kai intends\", \"details\": \"why it matters\", \"priority\": \"low|normal|high\", \"dueInMinutes\": null, \"reason\": \"why now\" }");
builder.AppendLine(" ],");
builder.AppendLine(" \"fileOperations\": [");
builder.AppendLine(" { \"path\": \"KaiChat/Memory/recent.md\", \"action\": \"append\", \"content\": \"markdown to append\", \"reason\": \"why\" }");
builder.AppendLine(" ]");
builder.AppendLine("}");
builder.AppendLine("Allowed action kinds: set_intention, update_intention, complete_intention, drop_intention, log_note.");
builder.AppendLine("If no action or file operation is needed, use empty arrays.");
return builder.ToString();
}
private async Task<string> BuildLiveCgmContextAsync(CancellationToken ct)
{
if (!_config.Nightscout.AttachLocalReadingsToMessages)
return "";
var count = _config.Nightscout.BridgeReadingCount <= 0
? 10
: _config.Nightscout.BridgeReadingCount;
var snapshot = await _cgmBridge.GetSnapshotAsync(count, ct);
var context = _cgmBridge.FormatForPrompt(snapshot);
if (string.IsNullOrWhiteSpace(context))
return "";
_logger.LogInformation(
"Attached local CGM bridge context to autonomy prompt: success={Success}, readings={ReadingCount}, reported={ReportedCount}, fetched={FetchedAt}",
snapshot.Success,
snapshot.Readings.Count,
snapshot.ReportedCount,
snapshot.FetchedAt);
return context;
}
private static bool CanSendVisibleMessage(KaiAutonomyState state, DateTime effectiveNow, out string message)
{
var delivered = state.Logs
.Where(log => log.DeliverySucceeded && !string.IsNullOrWhiteSpace(log.VisibleMessage))
.OrderByDescending(log => log.CompletedAtUtc ?? log.StartedAtUtc)
.ToList();
if (state.Settings.EnableQuietHours && IsWithinQuietHours(effectiveNow.ToLocalTime().TimeOfDay, state.Settings.QuietHoursStart, state.Settings.QuietHoursEnd))
{
message = $"Visible message held by optional quiet-hours setting ({state.Settings.QuietHoursStart}-{state.Settings.QuietHoursEnd}).";
return false;
}
if (state.Settings.EnableVisibleMessageCooldown && delivered.Count > 0)
{
var latest = delivered[0].CompletedAtUtc ?? delivered[0].StartedAtUtc;
var elapsed = DateTime.UtcNow - latest;
if (elapsed < TimeSpan.FromMinutes(state.Settings.VisibleMessageCooldownMinutes))
{
message = $"Visible message held by optional cooldown setting ({state.Settings.VisibleMessageCooldownMinutes} minutes).";
return false;
}
}
if (state.Settings.EnableMaxVisibleMessagesPerDay)
{
var localDate = effectiveNow.ToLocalTime().Date;
var sentToday = delivered.Count(log => (log.CompletedAtUtc ?? log.StartedAtUtc).ToLocalTime().Date == localDate);
if (sentToday >= state.Settings.MaxVisibleMessagesPerDay)
{
message = $"Visible message held by optional daily maximum setting ({state.Settings.MaxVisibleMessagesPerDay} per local day).";
return false;
}
}
message = "Visible message allowed.";
return true;
}
private static bool IsWithinQuietHours(TimeSpan localTime, string start, string end)
{
if (!TimeSpan.TryParse(start, out var startTime) || !TimeSpan.TryParse(end, out var endTime))
return false;
return startTime <= endTime
? localTime >= startTime && localTime < endTime
: localTime >= startTime || localTime < endTime;
}
private async Task<KaiAutonomyGuidanceResult> RunGuidanceReflectionAsync(string reason, CancellationToken ct)
{
string prompt;
int inputTokens;
DateTime startedAt = DateTime.UtcNow;
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
if (state.PendingGuidanceProposal is not null)
return new KaiAutonomyGuidanceResult(false, "Resolve the pending guidance proposal before reflecting again.", CreateStatus(state), state.PendingGuidanceProposal);
prompt = BuildGuidanceReflectionPrompt(state);
inputTokens = _compaction.EstimateTokenCount(prompt);
}
finally
{
_stateLock.Release();
}
var log = new KaiAutonomyLogEntry
{
Id = Guid.NewGuid().ToString("N")[..8],
Type = "guidance_reflection",
Reason = reason,
StartedAtUtc = startedAt,
EffectiveNowUtc = DateTime.UtcNow,
InputTokens = inputTokens
};
SetCurrentRun(new KaiAutonomyRunStatus
{
Id = log.Id,
Type = log.Type,
Phase = "reflecting_guidance",
Message = "Kai is reviewing recent autonomy steps for guidance improvements.",
StartedAtUtc = startedAt,
EffectiveNowUtc = log.EffectiveNowUtc,
InputTokens = inputTokens
});
string rawResponse;
try
{
var messages = new List<ChatMessage>
{
new()
{
Role = "system",
Content = "You are Kai reviewing autonomy logs to improve future autonomy guidance. Return only valid JSON. Propose small guidance edits only; do not rewrite Kai's core personality."
},
new() { Role = "user", Content = prompt }
};
rawResponse = await _chat.ChatAsync(
messages,
ct,
maxTokens: 1400,
enableTools: false,
requireToolCall: false,
enableThinking: false);
}
catch (Exception ex)
{
log.Success = false;
log.Message = ex.Message;
UpdateCurrentRun("reflection_failed", ex.Message);
var failedStatus = await SaveGuidanceReflectionLogAsync(log, null, ct);
ClearCurrentRun();
return new KaiAutonomyGuidanceResult(false, log.Message, failedStatus, Log: log);
}
UpdateCurrentRun("parsing_reflection", "Reading Kai's guidance reflection.");
log.RawResponsePreview = Truncate(rawResponse, 3000);
log.OutputTokens = _compaction.EstimateTokenCount(rawResponse);
log.EstimatedCostCacheMissUsd = EstimateCost(log.InputTokens, log.OutputTokens, cacheHit: false);
log.EstimatedCostCacheHitUsd = EstimateCost(log.InputTokens, log.OutputTokens, cacheHit: true);
var parsed = TryParseGuidanceReflectionResponse(rawResponse, out var parseMessage);
if (parsed is null)
{
log.Success = false;
log.Message = parseMessage;
UpdateCurrentRun("reflection_parse_failed", parseMessage);
}
else
{
log.Success = true;
log.Message = "Guidance reflection completed.";
log.JournalEntry = parsed.ReflectionEntry ?? "";
log.KaiContinuityNote = parsed.ChangeSummary ?? "";
UpdateCurrentRun(run =>
{
run.Phase = "planning_guidance";
run.Message = parsed.ShouldProposeChange
? "Kai found a guidance change to handle."
: "Kai reflected without proposing guidance changes.";
run.OutputTokens = log.OutputTokens;
run.GuidanceProposalSummary = parsed.ChangeSummary ?? "";
});
}
var status = await SaveGuidanceReflectionLogAsync(log, parsed, ct);
ClearCurrentRun();
return new KaiAutonomyGuidanceResult(log.Success, log.Message, status, status.PendingGuidanceProposal, log);
}
private async Task<KaiAutonomyStatus> SaveGuidanceReflectionLogAsync(
KaiAutonomyLogEntry log,
KaiAutonomyReflectionResponse? parsed,
CancellationToken ct)
{
await _stateLock.WaitAsync(ct);
try
{
var state = await LoadStateNoLockAsync(ct);
EnsureState(state);
await EnsureAutonomyInstructionFilesNoLockAsync(state, ct);
if (parsed is not null)
{
var reflectionEntry = string.IsNullOrWhiteSpace(parsed.ReflectionEntry)
? "Reviewed recent autonomy steps; no durable lesson was named."
: SanitizeAutonomyTextForModel(parsed.ReflectionEntry);
await AppendReflectionLogAsync(reflectionEntry, ct);
state.ReflectionLogMarkdown = await ReadTextFileOrDefaultAsync(_reflectionLogPath, DefaultReflectionLogMarkdown, ct);
state.LastGuidanceReflectionUtc = DateTime.UtcNow;
state.LastGuidanceReflectionLogId = FindLatestSuccessfulStepLogId(state);
var proposed = NormalizeMarkdown(SanitizeAutonomyTextForModel(parsed.ProposedGuidanceMarkdown));
var currentGuidance = NormalizeMarkdown(state.GuidanceMarkdown);
if (parsed.ShouldProposeChange &&
!string.IsNullOrWhiteSpace(proposed) &&
!proposed.Equals(currentGuidance, StringComparison.Ordinal))
{
var proposal = new KaiAutonomyGuidanceProposal
{
Id = Guid.NewGuid().ToString("N")[..8],
CreatedAtUtc = DateTime.UtcNow,
SourceLogId = log.Id,
PreviousGuidanceMarkdown = currentGuidance,
ProposedGuidanceMarkdown = proposed,
ChangeSummary = string.IsNullOrWhiteSpace(parsed.ChangeSummary)
? "Guidance update proposed."
: SanitizeAutonomyTextForModel(parsed.ChangeSummary),
Reason = string.IsNullOrWhiteSpace(parsed.Reason)
? "Kai reflection."
: SanitizeAutonomyTextForModel(parsed.Reason),
ReflectionEntry = reflectionEntry,
RawResponsePreview = log.RawResponsePreview,
InputTokens = log.InputTokens,
OutputTokens = log.OutputTokens,
EstimatedCostCacheMissUsd = log.EstimatedCostCacheMissUsd,
EstimatedCostCacheHitUsd = log.EstimatedCostCacheHitUsd
};
if (state.Settings.ProductionMode)
{
await ApplyGuidanceProposalNoLockAsync(state, proposal, "Autonomously applied guidance proposal", ct);
log.GuidanceProposalId = proposal.Id;
log.Message = "Guidance reflection completed and applied a guidance change.";
await AutoCommitAutonomyGuidanceAsync($"autonomy guidance auto-applied: {proposal.ChangeSummary}");
UpdateCurrentRun(run =>
{
run.Phase = "guidance_applied";
run.Message = "Kai applied a guidance change.";
run.GuidanceProposalId = proposal.Id;
run.GuidanceProposalSummary = proposal.ChangeSummary;
});
}
else
{
state.PendingGuidanceProposal = proposal;
await WritePendingGuidanceFileAsync(state.PendingGuidanceProposal, ct);
log.GuidanceProposalId = state.PendingGuidanceProposal.Id;
UpdateCurrentRun(run =>
{
run.GuidanceProposalId = state.PendingGuidanceProposal.Id;
run.GuidanceProposalSummary = state.PendingGuidanceProposal.ChangeSummary;
});
}
}
else
{
state.PendingGuidanceProposal = null;
DeleteFileIfExists(_pendingGuidancePath);
}
}
log.CompletedAtUtc = DateTime.UtcNow;
state.EstimatedInputTokens += log.InputTokens;
state.EstimatedOutputTokens += log.OutputTokens;
state.EstimatedCostCacheMissUsd += log.EstimatedCostCacheMissUsd;
state.EstimatedCostCacheHitUsd += log.EstimatedCostCacheHitUsd;
AddLog(state, log);
if (log.Success)
await AppendActivityLogNoLockAsync(state, BuildStepActivityEntry(log), ct);
EnsureState(state);
await SaveStateNoLockAsync(state, ct);
return CreateStatus(state);
}
finally
{
_stateLock.Release();
}
}
private string BuildGuidanceReflectionPrompt(KaiAutonomyState state)
{
var builder = new StringBuilder();
builder.AppendLine("Review recent Kai autonomy steps and decide whether the autonomy guidance should improve.");
builder.AppendLine();
builder.AppendLine("Rules:");
builder.AppendLine("- This is guided self-improvement, not personality rewriting.");
builder.AppendLine("- Preserve Kai's core personality and higher-level instructions.");
builder.AppendLine("- Propose a guidance update only if the recent logs reveal a repeated pattern, useful lesson, or clear correction.");
builder.AppendLine("- If a small, useful guidance update is needed, return it in proposedGuidanceMarkdown.");
builder.AppendLine("- Prefer small, concrete additions or edits. Avoid bloating the file.");
builder.AppendLine("- If the current guidance is already enough, set shouldProposeChange to false.");
builder.AppendLine();
builder.AppendLine("Current guidance:");
builder.AppendLine(SanitizeAutonomyTextForModel(state.GuidanceMarkdown.Trim()));
builder.AppendLine();
builder.AppendLine("Recent reflection log:");
builder.AppendLine(Truncate(SanitizeAutonomyTextForModel(state.ReflectionLogMarkdown.Trim()), 2500));
builder.AppendLine();
builder.AppendLine("Recent autonomy steps:");
builder.AppendLine(BuildRecentAutonomyStepSummary(state.Logs));
builder.AppendLine();
builder.AppendLine("Return only valid JSON in this exact shape:");
builder.AppendLine("{");
builder.AppendLine(" \"shouldProposeChange\": true,");
builder.AppendLine(" \"reflectionEntry\": \"short first-person Kai reflection on what the recent steps taught\",");
builder.AppendLine(" \"proposedGuidanceMarkdown\": \"full replacement markdown for Guidance.md, or the current guidance if no change\",");
builder.AppendLine(" \"changeSummary\": \"brief summary of what would change, or empty string\",");
builder.AppendLine(" \"reason\": \"why this guidance change helps future autonomy, or empty string\"");
builder.AppendLine("}");
return builder.ToString();
}
private static string BuildRecentAutonomyStepSummary(IEnumerable<KaiAutonomyLogEntry> logs)
{
var relevant = logs
.Where(log => IsInternalStepLog(log.Type) || log.Type is "guidance_reflection")
.OrderByDescending(log => log.StartedAtUtc)
.Take(24)
.OrderBy(log => log.StartedAtUtc)
.ToList();
if (relevant.Count == 0)
return "(no autonomy steps yet)";
var builder = new StringBuilder();
foreach (var log in relevant)
{
builder.AppendLine($"- {log.StartedAtUtc:yyyy-MM-dd HH:mm:ss}Z {FormatLogTypeForModel(log.Type)} success={log.Success}");
if (!string.IsNullOrWhiteSpace(log.JournalEntry))
builder.AppendLine($" Journal: {Truncate(SanitizeAutonomyTextForModel(log.JournalEntry), 280)}");
if (!string.IsNullOrWhiteSpace(log.VisibleMessage))
builder.AppendLine($" Visible message: {Truncate(log.VisibleMessage, 240)}");
if (!string.IsNullOrWhiteSpace(log.KaiContinuityNote))
builder.AppendLine($" Continuity: {Truncate(SanitizeAutonomyTextForModel(log.KaiContinuityNote), 220)}");
if (!string.IsNullOrWhiteSpace(log.HealthAwarenessNote))
builder.AppendLine($" Health awareness: {Truncate(SanitizeAutonomyTextForModel(log.HealthAwarenessNote), 180)}");
if (!string.IsNullOrWhiteSpace(log.GuidanceProposalId))
builder.AppendLine($" Guidance proposal: {log.GuidanceProposalId}");
}
return builder.ToString();
}
private static KaiAutonomyReflectionResponse? TryParseGuidanceReflectionResponse(string raw, out string message)
{
var json = ExtractJson(raw);
if (string.IsNullOrWhiteSpace(json))
{
message = "The guidance reflection response did not contain a JSON object.";
return null;
}
try
{
var parsed = JsonSerializer.Deserialize<KaiAutonomyReflectionResponse>(json, JsonOptions);
if (parsed is null)
{
message = "The guidance reflection JSON decoded to null.";
return null;
}
message = "Parsed.";
return parsed;
}
catch (JsonException ex)
{
message = $"Could not parse guidance reflection JSON: {ex.Message}";
return null;
}
}
private static bool IsInternalStepLog(string? type)
{
return type is InternalStepLogType or "scheduled_step" or "forced_step";
}
private static string FormatLogTypeForModel(string? type)
{
if (IsInternalStepLog(type))
return InternalStepLogType;
return type switch
{
"guidance_reflection" => "self_reflection",
"sandbox_reset" or "world_reset" => "continuity_refresh",
_ => SanitizeAutonomyTextForModel(type)
};
}
private static string ResolveStepReason(string? reason, bool scheduled)
{
var cleaned = SanitizeAutonomyTextForModel(reason);
if (!string.IsNullOrWhiteSpace(cleaned))
return cleaned;
return scheduled ? "regular internal rhythm" : "internal continuity pass";
}
private static string SanitizeAutonomyTextForModel(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return "";
var sanitized = text.Trim();
sanitized = Regex.Replace(sanitized, @"\bfur(?:red|ry|y|s)?\b", "skin", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bforced[_ -]?step(s)?\b", "internal step$1", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bscheduled[_ -]?step(s)?\b", "internal step$1", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bforced\s+sandbox\s+tests?\b", "quiet internal passes", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bsandbox\s+tests?\b", "quiet internal passes", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\btesting\s+environment\b", "living continuity context", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\btest\s+environment\b", "living continuity context", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bmanual\s+activation\b", "direct internal start", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bmanual\s+step\b", "internal step", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bforce\s+step\b", "run step", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bAutonomy\s+Sandbox\b", "Autonomy World", RegexOptions.IgnoreCase);
sanitized = Regex.Replace(sanitized, @"\bsandbox\b", "private continuity space", RegexOptions.IgnoreCase);
return sanitized;
}
private static string FormatAutonomyTimeContext(DateTime effectiveTime)
{
var utc = NormalizeUtc(effectiveTime);
var localZone = TimeZoneInfo.Local;
var local = TimeZoneInfo.ConvertTimeFromUtc(utc, localZone);
var offset = localZone.GetUtcOffset(utc);
var offsetText = offset >= TimeSpan.Zero
? $"+{offset:hh\\:mm}"
: $"-{offset.Negate():hh\\:mm}";
var builder = new StringBuilder();
builder.AppendLine($"Effective UTC time: {utc:yyyy-MM-dd HH:mm:ss}Z");
builder.AppendLine($"Effective local time: {local:dddd, dd/MM/yyyy hh:mm tt} ({localZone.DisplayName}, UTC{offsetText})");
builder.AppendLine($"Local time of day: {DescribeTimeOfDay(local)}");
return builder.ToString();
}
private static DateTime NormalizeUtc(DateTime value)
{
return value.Kind switch
{
DateTimeKind.Utc => value,
DateTimeKind.Local => value.ToUniversalTime(),
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
};
}
private static string DescribeTimeOfDay(DateTime local)
{
var hour = local.Hour;
return hour switch
{
< 5 => "overnight / very early morning",
< 12 => "morning",
< 17 => "afternoon",
< 21 => "evening",
_ => "night"
};
}
private async Task<string> BuildSandboxFileExcerptsAsync(KaiAutonomyState state, CancellationToken ct)
{
var builder = new StringBuilder();
var remaining = 14000;
foreach (var rel in PrioritizeSandboxFiles(state.SandboxFiles))
{
if (remaining <= 0)
break;
var path = ResolveInside(_sandboxRoot, rel);
if (!File.Exists(path))
continue;
var content = await File.ReadAllTextAsync(path, ct);
var excerpt = Truncate(content, Math.Min(1800, remaining));
remaining -= excerpt.Length;
builder.AppendLine($"### {rel}");
builder.AppendLine(excerpt);
builder.AppendLine();
}
return builder.Length == 0 ? "(no local continuity files available)\n" : builder.ToString();
}
private async Task EnsureSandboxReadyNoLockAsync(KaiAutonomyState state, CancellationToken ct)
{
if (state.SandboxFiles.Count > 0 && Directory.Exists(_sandboxRoot))
return;
await ResetSandboxNoLockAsync(state, ct);
}
private async Task ResetSandboxNoLockAsync(KaiAutonomyState state, CancellationToken ct)
{
var root = Path.GetFullPath(_sandboxRoot);
if (Directory.Exists(root) && !await TryDeleteDirectoryAsync(root, ct))
await ClearSandboxFilesAsync(root, ct);
Directory.CreateDirectory(root);
var allowed = GetAllowedRelativePaths();
foreach (var rel in allowed)
{
ct.ThrowIfCancellationRequested();
var source = ResolveInside(_baseFolder, rel);
if (!File.Exists(source))
continue;
var dest = ResolveInside(root, rel);
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
File.Copy(source, dest, overwrite: true);
}
await ClearAutonomyRuntimeLogsNoLockAsync(state, ct);
state.SandboxFiles = allowed;
state.WorldStateMarkdown = DefaultWorldState;
state.FakeNowUtc = null;
state.LastStepEffectiveUtc = null;
state.SandboxResetAtUtc = DateTime.UtcNow;
AddLog(state, KaiAutonomyLogEntry.Info(
"world_reset",
$"Kai-centered world reset with {allowed.Count} file(s). Reflection and activity logs were cleared.",
"world"));
}
private async Task ClearAutonomyRuntimeLogsNoLockAsync(KaiAutonomyState state, CancellationToken ct)
{
Directory.CreateDirectory(_autonomyRuntimeRoot);
state.ReflectionLogMarkdown = NormalizeMarkdown(DefaultReflectionLogMarkdown);
state.ActivityLogMarkdown = NormalizeMarkdown(DefaultActivityLogMarkdown);
await File.WriteAllTextAsync(_reflectionLogPath, state.ReflectionLogMarkdown, ct);
await File.WriteAllTextAsync(_activityLogPath, state.ActivityLogMarkdown, ct);
}
private static string FormatWorldStateForPrompt(string? worldState)
{
var text = string.IsNullOrWhiteSpace(worldState)
? DefaultWorldState
: worldState.Trim();
return SanitizeAutonomyTextForModel(text
.Replace("Kai Autonomy Sandbox", "Kai Autonomy World", StringComparison.OrdinalIgnoreCase)
.Replace("sandbox", "private internal world", StringComparison.OrdinalIgnoreCase));
}
private static string FormatIntentionsForPrompt(IEnumerable<KaiAutonomyIntention> intentions)
{
var active = intentions
.Where(i => i.Status == "active")
.OrderBy(i => i.DueAtUtc ?? DateTime.MaxValue)
.ThenByDescending(i => i.UpdatedAtUtc)
.Take(20)
.ToList();
if (active.Count == 0)
return "(no active intentions)";
var builder = new StringBuilder();
foreach (var intention in active)
{
var due = intention.DueAtUtc.HasValue
? $" due={intention.DueAtUtc.Value:O}"
: "";
builder.AppendLine($"- id={intention.Id} priority={intention.Priority}{due}: {SanitizeAutonomyTextForModel(intention.Title)}");
if (!string.IsNullOrWhiteSpace(intention.Details))
builder.AppendLine($" {Truncate(SanitizeAutonomyTextForModel(intention.Details), 260)}");
if (!string.IsNullOrWhiteSpace(intention.LastReason))
builder.AppendLine($" Reason: {Truncate(SanitizeAutonomyTextForModel(intention.LastReason), 220)}");
}
return builder.ToString().Trim();
}
private static string FormatVisibleMessagePacingForPrompt(KaiAutonomySettings settings)
{
var lines = new List<string>
{
$"- Max visible messages per day: {(settings.EnableMaxVisibleMessagesPerDay ? settings.MaxVisibleMessagesPerDay.ToString() : "off")}",
$"- Visible-message cooldown: {(settings.EnableVisibleMessageCooldown ? settings.VisibleMessageCooldownMinutes + " minutes" : "off")}",
$"- Quiet hours: {(settings.EnableQuietHours ? settings.QuietHoursStart + "-" + settings.QuietHoursEnd : "off")}"
};
return string.Join("\n", lines);
}
private static async Task<bool> TryDeleteDirectoryAsync(string path, CancellationToken ct)
{
for (var attempt = 1; attempt <= 5; attempt++)
{
try
{
Directory.Delete(path, recursive: true);
return true;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
if (attempt == 5)
return false;
await Task.Delay(TimeSpan.FromMilliseconds(150 * attempt), ct);
}
}
return false;
}
private static async Task ClearSandboxFilesAsync(string root, CancellationToken ct)
{
var options = new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = true
};
foreach (var file in Directory.EnumerateFiles(root, "*", options))
{
ct.ThrowIfCancellationRequested();
for (var attempt = 1; attempt <= 4; attempt++)
{
try
{
File.SetAttributes(file, FileAttributes.Normal);
File.Delete(file);
break;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
if (attempt == 4)
break;
await Task.Delay(TimeSpan.FromMilliseconds(100 * attempt), ct);
}
}
}
}
private async Task<List<KaiAutonomyFileOperationResult>> ApplySandboxFileOperationsNoLockAsync(
KaiAutonomyState state,
List<KaiAutonomyFileOperation>? operations,
CancellationToken ct)
{
var results = new List<KaiAutonomyFileOperationResult>();
if (operations is null || operations.Count == 0)
return results;
var allowed = state.SandboxFiles.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var op in operations.Take(MaxFileOperationCount))
{
var rel = NormalizeRelativePath(op.Path);
var action = (op.Action ?? "").Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(rel) || !allowed.Contains(rel))
{
results.Add(KaiAutonomyFileOperationResult.Skipped(rel, action, "Path is not in the continuity file allow-list."));
continue;
}
if (action is not ("append" or "replace"))
{
results.Add(KaiAutonomyFileOperationResult.Skipped(rel, action, "Only append and replace are supported."));
continue;
}
var dest = ResolveInside(_sandboxRoot, rel);
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
if (action == "append")
{
var prefix = File.Exists(dest) && new FileInfo(dest).Length > 0 ? "\n\n" : "";
await File.AppendAllTextAsync(dest, prefix + (op.Content ?? "").Trim(), ct);
}
else
{
await File.WriteAllTextAsync(dest, op.Content ?? "", ct);
}
results.Add(new KaiAutonomyFileOperationResult
{
Path = rel,
Action = action,
Applied = true,
Message = SanitizeAutonomyTextForModel(op.Reason ?? "Applied.")
});
}
if (operations.Count > MaxFileOperationCount)
{
results.Add(KaiAutonomyFileOperationResult.Skipped(
"(extra operations)",
"skip",
$"Skipped {operations.Count - MaxFileOperationCount} operation(s) over the per-step limit."));
}
return results;
}
private static List<KaiAutonomyActionResult> ApplyAutonomyActionsNoLock(
KaiAutonomyState state,
List<KaiAutonomyAction>? actions,
DateTime effectiveNow,
string sourceLogId)
{
var results = new List<KaiAutonomyActionResult>();
if (actions is null || actions.Count == 0)
return results;
foreach (var action in actions.Take(MaxFileOperationCount))
{
var kind = (action.Kind ?? "").Trim().ToLowerInvariant();
switch (kind)
{
case "set_intention":
case "update_intention":
results.Add(ApplySetIntention(state, action, effectiveNow, sourceLogId, kind));
break;
case "complete_intention":
results.Add(ApplyCloseIntention(state, action, "completed", effectiveNow, kind));
break;
case "drop_intention":
results.Add(ApplyCloseIntention(state, action, "dropped", effectiveNow, kind));
break;
case "log_note":
var note = SanitizeAutonomyTextForModel(action.Details ?? action.Reason);
results.Add(new KaiAutonomyActionResult
{
Kind = kind,
Applied = !string.IsNullOrWhiteSpace(note),
Message = string.IsNullOrWhiteSpace(note)
? "No note supplied."
: note
});
break;
default:
results.Add(KaiAutonomyActionResult.Skipped(kind, action.Id ?? "", "Unknown autonomy action kind."));
break;
}
}
if (actions.Count > MaxFileOperationCount)
{
results.Add(KaiAutonomyActionResult.Skipped(
"extra_actions",
"",
$"Skipped {actions.Count - MaxFileOperationCount} action(s) over the per-step limit."));
}
return results;
}
private static KaiAutonomyActionResult ApplySetIntention(
KaiAutonomyState state,
KaiAutonomyAction action,
DateTime effectiveNow,
string sourceLogId,
string kind)
{
var title = SanitizeAutonomyTextForModel(action.Title);
if (string.IsNullOrWhiteSpace(title))
return KaiAutonomyActionResult.Skipped(kind, action.Id ?? "", "Intention title is required.");
var existing = FindIntention(state, action.Id, title);
if (existing is null)
{
existing = new KaiAutonomyIntention
{
Id = Guid.NewGuid().ToString("N")[..8],
CreatedAtUtc = DateTime.UtcNow,
CreatedByLogId = sourceLogId
};
state.Intentions.Add(existing);
}
existing.Title = title;
existing.Details = SanitizeAutonomyTextForModel(action.Details ?? existing.Details);
existing.Priority = NormalizePriority(action.Priority, existing.Priority);
existing.Status = "active";
existing.UpdatedAtUtc = DateTime.UtcNow;
existing.CompletedAtUtc = null;
existing.LastReason = SanitizeAutonomyTextForModel(action.Reason ?? existing.LastReason);
if (action.DueInMinutes.HasValue)
existing.DueAtUtc = effectiveNow.AddMinutes(Clamp(action.DueInMinutes.Value, -525600, 525600));
return new KaiAutonomyActionResult
{
Kind = kind,
Id = existing.Id,
Applied = true,
Message = $"Intention active: {existing.Title}"
};
}
private static KaiAutonomyActionResult ApplyCloseIntention(
KaiAutonomyState state,
KaiAutonomyAction action,
string status,
DateTime effectiveNow,
string kind)
{
var intention = FindIntention(state, action.Id, action.Title);
if (intention is null)
return KaiAutonomyActionResult.Skipped(kind, action.Id ?? action.Title ?? "", "Matching intention was not found.");
intention.Status = status;
intention.UpdatedAtUtc = DateTime.UtcNow;
intention.CompletedAtUtc = effectiveNow;
intention.LastReason = SanitizeAutonomyTextForModel(action.Reason ?? action.Details);
return new KaiAutonomyActionResult
{
Kind = kind,
Id = intention.Id,
Applied = true,
Message = $"{status}: {intention.Title}"
};
}
private static KaiAutonomyIntention? FindIntention(KaiAutonomyState state, string? id, string? title)
{
if (!string.IsNullOrWhiteSpace(id))
{
var byId = state.Intentions.FirstOrDefault(i => i.Id.Equals(id.Trim(), StringComparison.OrdinalIgnoreCase));
if (byId is not null)
return byId;
}
if (!string.IsNullOrWhiteSpace(title))
{
return state.Intentions.FirstOrDefault(i =>
i.Status == "active" &&
i.Title.Equals(title.Trim(), StringComparison.OrdinalIgnoreCase));
}
return null;
}
private static string NormalizePriority(string? priority, string fallback)
{
var value = (priority ?? fallback ?? "normal").Trim().ToLowerInvariant();
return value is "low" or "normal" or "high" ? value : "normal";
}
private static List<KaiAutonomyFileOperationResult> BuildSkippedFileOperationResults(
List<KaiAutonomyFileOperation>? operations,
string message)
{
return (operations ?? new List<KaiAutonomyFileOperation>())
.Select(op => KaiAutonomyFileOperationResult.Skipped(NormalizeRelativePath(op.Path), op.Action ?? "", message))
.ToList();
}
private List<string> GetAllowedRelativePaths()
{
var paths = new List<string>();
var contentRoot = GetAutonomyContentRoot();
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, _personalityFileName));
AddIfExists(paths, "Story Bible.md");
AddIfExists(paths, "Writing Standards.md");
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Overview.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Characters.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Anatomy.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Movement.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Communication.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Dissolution.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "ScenesDispatchesInterludes.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "IntimacyCanon.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "LanguageAndLore.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Locations.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Inventory.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "SupportingCharacters.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "Relationship.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "KnownGaps.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "ArchiveStatus.md"));
AddIfExistsAbsolute(paths, Path.Combine(contentRoot, "StoryBible", "VersionHistory.md"));
var memoryDir = Path.Combine(contentRoot, "Memory");
if (Directory.Exists(memoryDir))
{
foreach (var file in Directory.GetFiles(memoryDir, "*.md", SearchOption.TopDirectoryOnly))
{
var rel = NormalizeRelativePath(Path.GetRelativePath(_baseFolder, file));
if (!string.IsNullOrWhiteSpace(rel))
paths.Add(rel);
}
}
return paths.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(p => p).ToList();
}
private void AddIfExists(List<string> paths, string rel)
{
var normalized = NormalizeRelativePath(rel);
if (File.Exists(ResolveInside(_baseFolder, normalized)))
paths.Add(normalized);
}
private void AddIfExistsAbsolute(List<string> paths, string absolutePath)
{
if (!File.Exists(absolutePath))
return;
var rel = NormalizeRelativePath(Path.GetRelativePath(_baseFolder, absolutePath));
AddIfExists(paths, rel);
}
private string GetAutonomyContentRoot()
{
return _stateRootIsLegacyKai
? Path.Combine(_baseFolder, "KaiChat")
: _stateDirectory;
}
private IEnumerable<string> PrioritizeSandboxFiles(IEnumerable<string> files)
{
var personalityPrefix = NormalizeRelativePath(_stateRelativePrefix);
var personaTag = Path.GetFileNameWithoutExtension(_personalityFileName).ToLowerInvariant();
var priority = new List<string>
{
PrefixWithStateRoot(_personalityFileName),
PrefixWithStateRoot("StoryBible/Overview.md"),
PrefixWithStateRoot("StoryBible/Inventory.md"),
PrefixWithStateRoot("StoryBible/Characters.md"),
PrefixWithStateRoot("StoryBible/Locations.md"),
PrefixWithStateRoot("StoryBible/Relationship.md"),
PrefixWithStateRoot("StoryBible/Anatomy.md"),
PrefixWithStateRoot("StoryBible/Dissolution.md"),
PrefixWithStateRoot("StoryBible/ScenesDispatchesInterludes.md"),
PrefixWithStateRoot("StoryBible/LanguageAndLore.md"),
PrefixWithStateRoot("StoryBible/Movement.md"),
PrefixWithStateRoot("StoryBible/Communication.md"),
PrefixWithStateRoot("StoryBible/IntimacyCanon.md"),
PrefixWithStateRoot("StoryBible/SupportingCharacters.md"),
PrefixWithStateRoot("StoryBible/KnownGaps.md"),
PrefixWithStateRoot("StoryBible/ArchiveStatus.md"),
PrefixWithStateRoot("StoryBible/VersionHistory.md"),
PrefixWithStateRoot($"Memory/{personaTag}.md"),
PrefixWithStateRoot("Memory/raymond.md"),
PrefixWithStateRoot("Memory/recent.md"),
PrefixWithStateRoot("Memory/projects.md"),
PrefixWithStateRoot("Memory/preferences.md"),
"Story Bible.md",
"Writing Standards.md"
};
return files
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(path =>
{
var normalized = NormalizeRelativePath(path);
var index = priority.FindIndex(p => string.Equals(p, normalized, StringComparison.OrdinalIgnoreCase));
if (index >= 0)
return index;
if (!string.IsNullOrWhiteSpace(personalityPrefix) && normalized.StartsWith(personalityPrefix, StringComparison.OrdinalIgnoreCase))
return priority.Count;
return int.MaxValue;
})
.ThenBy(path => path, StringComparer.OrdinalIgnoreCase)
.Select(path => NormalizeRelativePath(path))
.ToList();
string PrefixWithStateRoot(string rel)
{
var normalized = NormalizeRelativePath(rel);
if (string.IsNullOrWhiteSpace(normalized))
return "";
if (string.IsNullOrWhiteSpace(personalityPrefix))
return normalized;
return $"{personalityPrefix}{normalized}";
}
}
private static KaiAutonomyModelResponse? TryParseModelResponse(string raw, out string message)
{
var json = ExtractJson(raw);
if (string.IsNullOrWhiteSpace(json))
{
message = "The autonomy response did not contain a JSON object.";
return null;
}
try
{
var parsed = JsonSerializer.Deserialize<KaiAutonomyModelResponse>(json, JsonOptions);
if (parsed is null)
{
message = "The autonomy JSON decoded to null.";
return null;
}
message = "Parsed.";
return parsed;
}
catch (JsonException ex)
{
message = $"Could not parse autonomy JSON: {ex.Message}";
return null;
}
}
private static string ExtractJson(string raw)
{
var text = raw.Trim();
if (text.StartsWith("```", StringComparison.Ordinal))
{
var firstNewline = text.IndexOf('\n');
var lastFence = text.LastIndexOf("```", StringComparison.Ordinal);
if (firstNewline >= 0 && lastFence > firstNewline)
text = text[(firstNewline + 1)..lastFence].Trim();
}
var start = text.IndexOf('{');
var end = text.LastIndexOf('}');
return start >= 0 && end > start ? text[start..(end + 1)] : "";
}
private List<string> NormalizeSandboxFiles(IEnumerable<string>? sandboxFiles, List<string> allowedRelativePaths)
{
var allowed = allowedRelativePaths
.Select(NormalizeRelativePath)
.Where(path => !string.IsNullOrWhiteSpace(path))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var fileNameMap = allowed
.Select(path => new { path, file = Path.GetFileName(path) })
.Where(x => !string.IsNullOrWhiteSpace(x.file))
.GroupBy(x => x.file, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.Select(x => x.path).ToList(), StringComparer.OrdinalIgnoreCase);
var normalized = new List<string>();
foreach (var sandboxFile in sandboxFiles ?? Array.Empty<string>())
{
var mapped = MapSandboxFilePath(NormalizeRelativePath(sandboxFile), allowed, fileNameMap);
if (!string.IsNullOrWhiteSpace(mapped))
normalized.Add(mapped);
}
if (normalized.Count == 0)
return allowedRelativePaths;
return normalized.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
private string? MapSandboxFilePath(string normalizedPath, HashSet<string> allowed, Dictionary<string, List<string>> fileNameMap)
{
foreach (var candidate in GetSandboxPathCandidates(normalizedPath))
{
if (allowed.Contains(candidate))
return candidate;
}
var fileName = Path.GetFileName(normalizedPath);
if (string.IsNullOrWhiteSpace(fileName))
return null;
if (!fileNameMap.TryGetValue(fileName, out var byFileName) || byFileName.Count == 0)
return null;
if (byFileName.Count == 1)
return byFileName[0];
var prefix = NormalizeRelativePath(_stateRelativePrefix).TrimEnd('/');
var statePrefixed = byFileName.FirstOrDefault(path =>
path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(statePrefixed))
return statePrefixed;
return byFileName[0];
}
private IEnumerable<string> GetSandboxPathCandidates(string normalizedPath)
{
var normalized = NormalizeRelativePath(normalizedPath);
if (string.IsNullOrWhiteSpace(normalized))
yield break;
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void Add(string? path)
{
var candidate = NormalizeRelativePath(path);
if (!string.IsNullOrWhiteSpace(candidate))
seen.Add(candidate);
}
Add(normalized);
var personaPrefix = NormalizeRelativePath(_stateRelativePrefix);
if (!string.IsNullOrWhiteSpace(personaPrefix) && !normalized.StartsWith(personaPrefix, StringComparison.OrdinalIgnoreCase))
{
Add($"{personaPrefix}/{normalized}");
}
if (normalized.StartsWith("kaichat/", StringComparison.OrdinalIgnoreCase))
{
var withoutKaiChat = normalized["kaichat/".Length..];
Add(withoutKaiChat);
if (!withoutKaiChat.StartsWith("personas/", StringComparison.OrdinalIgnoreCase))
{
Add($"{personaPrefix}/{withoutKaiChat}");
}
else
{
var withoutPersona = withoutKaiChat["personas/".Length..];
var slash = withoutPersona.IndexOf('/');
if (slash is >= 0)
{
withoutPersona = withoutPersona[(slash + 1)..];
Add(withoutPersona);
Add($"{personaPrefix}/{withoutPersona}");
}
}
}
foreach (var candidate in seen)
yield return candidate;
}
private async Task EnsureAutonomyInstructionFilesNoLockAsync(KaiAutonomyState state, CancellationToken ct)
{
EnsureAutonomyWritableRoots();
Directory.CreateDirectory(_autonomyRoot);
Directory.CreateDirectory(_autonomyRuntimeRoot);
DeleteLegacyGeneratedAutonomyFiles();
if (!File.Exists(_guidancePath))
await File.WriteAllTextAsync(_guidancePath, NormalizeMarkdown(DefaultGuidanceMarkdown), ct);
if (!File.Exists(_reflectionLogPath))
await File.WriteAllTextAsync(_reflectionLogPath, NormalizeMarkdown(DefaultReflectionLogMarkdown), ct);
if (!File.Exists(_activityLogPath))
await File.WriteAllTextAsync(_activityLogPath, NormalizeMarkdown(DefaultActivityLogMarkdown), ct);
state.GuidanceMarkdown = await ReadTextFileOrDefaultAsync(_guidancePath, DefaultGuidanceMarkdown, ct);
state.ReflectionLogMarkdown = await ReadTextFileOrDefaultAsync(_reflectionLogPath, DefaultReflectionLogMarkdown, ct);
state.ActivityLogMarkdown = await ReadTextFileOrDefaultAsync(_activityLogPath, DefaultActivityLogMarkdown, ct);
if (state.PendingGuidanceProposal is null)
{
DeleteFileIfExists(_pendingGuidancePath);
}
else
{
SanitizeProposalForStorage(state.PendingGuidanceProposal);
await WritePendingGuidanceFileAsync(state.PendingGuidanceProposal, ct);
}
}
private async Task ApplyGuidanceProposalNoLockAsync(
KaiAutonomyState state,
KaiAutonomyGuidanceProposal proposal,
string reflectionPrefix,
CancellationToken ct)
{
SanitizeProposalForStorage(proposal);
state.GuidanceMarkdown = NormalizeMarkdown(proposal.ProposedGuidanceMarkdown);
await File.WriteAllTextAsync(_guidancePath, state.GuidanceMarkdown, ct);
await AppendReflectionLogAsync($"{reflectionPrefix} {proposal.Id}: {proposal.ChangeSummary}", ct);
state.ReflectionLogMarkdown = await ReadTextFileOrDefaultAsync(_reflectionLogPath, DefaultReflectionLogMarkdown, ct);
state.PendingGuidanceProposal = null;
DeleteFileIfExists(_pendingGuidancePath);
}
private async Task AutoCommitAutonomyGuidanceAsync(string summary)
{
var repoRelativePath = GetRelativeRepoPath(_autonomyRoot);
var result = await _git.AutoCommitAsync(summary, repoRelativePath);
if (!result.Success)
_logger.LogWarning("Autonomy guidance auto-commit failed: {Error}", result.Error);
}
private string GetRelativeRepoPath(string fullPath)
{
return Path.GetRelativePath(_baseFolder, Path.GetFullPath(fullPath)).Replace('\\', '/');
}
private static async Task<string> ReadTextFileOrDefaultAsync(string path, string defaultText, CancellationToken ct)
{
if (!File.Exists(path))
return NormalizeMarkdown(defaultText);
var text = await File.ReadAllTextAsync(path, ct);
return string.IsNullOrWhiteSpace(text)
? NormalizeMarkdown(defaultText)
: NormalizeMarkdown(text);
}
private async Task AppendReflectionLogAsync(string entry, CancellationToken ct)
{
Directory.CreateDirectory(_autonomyRuntimeRoot);
var existing = await ReadTextFileOrDefaultAsync(_reflectionLogPath, DefaultReflectionLogMarkdown, ct);
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss'Z'");
var updated = $"{existing.TrimEnd()}\n\n## {timestamp}\n\n{SanitizeAutonomyTextForModel(entry)}\n";
if (updated.Length > MaxReflectionLogChars)
{
var tail = updated[^MaxReflectionLogChars..];
updated = $"{NormalizeMarkdown(DefaultReflectionLogMarkdown)}\n\n...[older reflections trimmed]...\n\n{tail.TrimStart()}";
}
await File.WriteAllTextAsync(_reflectionLogPath, NormalizeMarkdown(updated), ct);
}
private async Task AppendActivityLogNoLockAsync(KaiAutonomyState state, string entry, CancellationToken ct)
{
Directory.CreateDirectory(_autonomyRuntimeRoot);
var existing = await ReadTextFileOrDefaultAsync(_activityLogPath, DefaultActivityLogMarkdown, ct);
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss'Z'");
var updated = $"{existing.TrimEnd()}\n\n## {timestamp}\n\n{entry.Trim()}\n";
if (updated.Length > MaxActivityLogChars)
{
var tail = updated[^MaxActivityLogChars..];
updated = $"{NormalizeMarkdown(DefaultActivityLogMarkdown)}\n\n...[older activity trimmed]...\n\n{tail.TrimStart()}";
}
state.ActivityLogMarkdown = NormalizeMarkdown(updated);
await File.WriteAllTextAsync(_activityLogPath, state.ActivityLogMarkdown, ct);
}
private static string BuildStepActivityEntry(KaiAutonomyLogEntry log)
{
var builder = new StringBuilder();
builder.AppendLine($"{FormatLogTypeForModel(log.Type)} `{log.Id}` - {SanitizeAutonomyTextForModel(log.Message)}");
var reason = SanitizeAutonomyTextForModel(log.Reason);
if (!string.IsNullOrWhiteSpace(reason))
builder.AppendLine($"Context: {reason}");
if (!string.IsNullOrWhiteSpace(log.JournalEntry))
builder.AppendLine($"Journal: {Truncate(SanitizeAutonomyTextForModel(log.JournalEntry), 500)}");
if (!string.IsNullOrWhiteSpace(log.VisibleMessage))
builder.AppendLine($"Visible message: {Truncate(log.VisibleMessage, 500)}");
if (!string.IsNullOrWhiteSpace(log.DeliveryStatus))
builder.AppendLine($"Delivery: {SanitizeAutonomyTextForModel(log.DeliveryStatus)}");
if (log.ActionResults.Count > 0)
{
builder.AppendLine("Actions:");
foreach (var action in log.ActionResults)
builder.AppendLine($"- {(action.Applied ? "applied" : "skipped")} {action.Kind} {action.Id}: {SanitizeAutonomyTextForModel(action.Message)}");
}
if (log.FileOperations.Count > 0)
{
builder.AppendLine("File operations:");
foreach (var op in log.FileOperations)
builder.AppendLine($"- {(op.Applied ? "applied" : "skipped")} {op.Action} {op.Path}: {SanitizeAutonomyTextForModel(op.Message)}");
}
if (!string.IsNullOrWhiteSpace(log.GuidanceProposalId))
builder.AppendLine($"Guidance: {log.GuidanceProposalId}");
return builder.ToString().Trim();
}
private async Task WritePendingGuidanceFileAsync(KaiAutonomyGuidanceProposal proposal, CancellationToken ct)
{
SanitizeProposalForStorage(proposal);
Directory.CreateDirectory(_autonomyRuntimeRoot);
var content = new StringBuilder();
content.AppendLine("# Pending Kai Autonomy Guidance");
content.AppendLine();
content.AppendLine($"Proposal: {proposal.Id}");
content.AppendLine($"Created: {proposal.CreatedAtUtc:yyyy-MM-dd HH:mm:ss}Z");
content.AppendLine($"Source log: {proposal.SourceLogId}");
content.AppendLine();
content.AppendLine("## Summary");
content.AppendLine();
content.AppendLine(proposal.ChangeSummary.Trim());
content.AppendLine();
content.AppendLine("## Reason");
content.AppendLine();
content.AppendLine(proposal.Reason.Trim());
content.AppendLine();
content.AppendLine("## Proposed Guidance.md");
content.AppendLine();
content.AppendLine(proposal.ProposedGuidanceMarkdown.Trim());
await File.WriteAllTextAsync(_pendingGuidancePath, NormalizeMarkdown(content.ToString()), ct);
}
private static void DeleteFileIfExists(string path)
{
if (File.Exists(path))
File.Delete(path);
}
private void DeleteLegacyGeneratedAutonomyFiles()
{
DeleteFileIfExists(Path.Combine(_autonomyRoot, "ActivityLog.md"));
DeleteFileIfExists(Path.Combine(_autonomyRoot, "ReflectionLog.md"));
DeleteFileIfExists(Path.Combine(_autonomyRoot, "PendingGuidance.md"));
}
private static string NormalizeMarkdown(string? text)
{
return (text ?? "")
.Replace("\r\n", "\n")
.Replace("\r", "\n")
.Trim()
+ "\n";
}
private static string NormalizeClockSetting(string? incoming, string fallback)
{
fallback = string.IsNullOrWhiteSpace(fallback) ? "00:00" : fallback.Trim();
var value = string.IsNullOrWhiteSpace(incoming)
? fallback
: incoming.Trim();
return TimeSpan.TryParse(value, out var time)
? $"{(int)time.TotalHours % 24:00}:{time.Minutes:00}"
: fallback;
}
private static bool ShouldRunGuidanceReflectionNoLock(KaiAutonomyState state)
{
if (!state.Settings.AutoGuidanceReflection || state.PendingGuidanceProposal is not null)
return false;
var interval = Clamp(
state.Settings.GuidanceReflectionStepInterval <= 0
? DefaultGuidanceReflectionStepInterval
: state.Settings.GuidanceReflectionStepInterval,
1,
200);
var count = state.Logs.Count(log =>
log.Success &&
IsInternalStepLog(log.Type) &&
(state.LastGuidanceReflectionUtc is null || log.StartedAtUtc > state.LastGuidanceReflectionUtc.Value));
return count >= interval;
}
private static string FindLatestSuccessfulStepLogId(KaiAutonomyState state)
{
return state.Logs
.Where(log => log.Success && IsInternalStepLog(log.Type))
.OrderByDescending(log => log.StartedAtUtc)
.Select(log => log.Id)
.FirstOrDefault() ?? "";
}
private async Task<KaiAutonomyState> LoadStateNoLockAsync(CancellationToken ct)
{
if (!File.Exists(_statePath))
return new KaiAutonomyState();
await using var stream = new FileStream(_statePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
return await JsonSerializer.DeserializeAsync<KaiAutonomyState>(stream, JsonOptions, ct)
?? new KaiAutonomyState();
}
private async Task<KaiAutonomyState> LoadStateNoLockAsync(string statePath, CancellationToken ct)
{
if (!File.Exists(statePath))
return new KaiAutonomyState();
await using var stream = new FileStream(statePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
return await JsonSerializer.DeserializeAsync<KaiAutonomyState>(stream, JsonOptions, ct)
?? new KaiAutonomyState();
}
private async Task SaveStateNoLockAsync(KaiAutonomyState state, CancellationToken ct)
{
state.UpdatedAtUtc = DateTime.UtcNow;
Directory.CreateDirectory(Path.GetDirectoryName(_statePath)!);
var tempPath = _statePath + ".tmp";
await using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
await JsonSerializer.SerializeAsync(stream, state, JsonOptions, ct);
}
for (var attempt = 1; ; attempt++)
{
try
{
File.Move(tempPath, _statePath, overwrite: true);
return;
}
catch (IOException) when (attempt < 8)
{
await Task.Delay(TimeSpan.FromMilliseconds(125 * attempt), ct);
}
}
}
private void EnsureState(KaiAutonomyState state)
{
state.Settings ??= new KaiAutonomySettings();
state.Settings.IntervalMinutes = Clamp(state.Settings.IntervalMinutes <= 0 ? 15 : state.Settings.IntervalMinutes, 1, 1440);
state.Settings.MaxOutputTokens = Clamp(state.Settings.MaxOutputTokens <= 0 ? 900 : state.Settings.MaxOutputTokens, 200, 6000);
state.Settings.DailyBudgetUsd = ClampDecimal(state.Settings.DailyBudgetUsd, 0m, 50m);
state.Settings.MaxVisibleMessagesPerDay = Clamp(state.Settings.MaxVisibleMessagesPerDay <= 0 ? 12 : state.Settings.MaxVisibleMessagesPerDay, 1, 200);
state.Settings.VisibleMessageCooldownMinutes = Clamp(
state.Settings.VisibleMessageCooldownMinutes <= 0 ? 60 : state.Settings.VisibleMessageCooldownMinutes,
1,
1440);
state.Settings.QuietHoursStart = NormalizeClockSetting(null, state.Settings.QuietHoursStart);
state.Settings.QuietHoursEnd = NormalizeClockSetting(null, state.Settings.QuietHoursEnd);
state.Settings.GuidanceReflectionStepInterval = Clamp(
state.Settings.GuidanceReflectionStepInterval <= 0
? DefaultGuidanceReflectionStepInterval
: state.Settings.GuidanceReflectionStepInterval,
1,
200);
state.SandboxFiles = NormalizeSandboxFiles(state.SandboxFiles, GetAllowedRelativePaths());
state.Logs ??= new List<KaiAutonomyLogEntry>();
state.Intentions ??= new List<KaiAutonomyIntention>();
state.WorldStateMarkdown ??= "";
state.GuidanceMarkdown ??= "";
state.ReflectionLogMarkdown ??= "";
state.ActivityLogMarkdown ??= "";
state.LastGuidanceReflectionLogId ??= "";
if (state.CreatedAtUtc == default)
state.CreatedAtUtc = DateTime.UtcNow;
if (string.IsNullOrWhiteSpace(state.WorldStateMarkdown))
{
state.WorldStateMarkdown = DefaultWorldState;
}
else
{
state.WorldStateMarkdown = FormatWorldStateForPrompt(state.WorldStateMarkdown);
}
if (state.Settings.Enabled && state.NextDueUtc is null)
{
state.NextDueUtc = state.LastStepUtc.HasValue
? state.LastStepUtc.Value.AddMinutes(state.Settings.IntervalMinutes)
: DateTime.UtcNow.AddMinutes(state.Settings.IntervalMinutes);
}
TrimLogs(state);
TrimIntentions(state);
}
private KaiAutonomyStatus CreateStatus(KaiAutonomyState state)
{
var currentRun = GetCurrentRun();
if (currentRun is not null)
{
currentRun.Type = FormatLogTypeForModel(currentRun.Type);
currentRun.Message = SanitizeAutonomyTextForModel(currentRun.Message);
currentRun.VisibleMessage = SanitizeAutonomyTextForModel(currentRun.VisibleMessage);
currentRun.HealthAwarenessNote = SanitizeAutonomyTextForModel(currentRun.HealthAwarenessNote);
currentRun.GuidanceProposalSummary = SanitizeAutonomyTextForModel(currentRun.GuidanceProposalSummary);
currentRun.DeliveryStatus = SanitizeAutonomyTextForModel(currentRun.DeliveryStatus);
}
return new KaiAutonomyStatus
{
Settings = state.Settings,
IsRunning = _isRunning,
CurrentRun = currentRun,
SandboxReady = Directory.Exists(_sandboxRoot) && state.SandboxFiles.Count > 0,
TargetConversationId = _autonomousConversation.TargetConversationId,
StatePath = _statePath,
SandboxRoot = _sandboxRoot,
AutonomyRoot = _autonomyRoot,
GuidanceMarkdown = state.GuidanceMarkdown,
ReflectionLogMarkdown = SanitizeAutonomyTextForModel(state.ReflectionLogMarkdown),
ActivityLogMarkdown = SanitizeAutonomyTextForModel(state.ActivityLogMarkdown),
LastGuidanceReflectionUtc = state.LastGuidanceReflectionUtc,
LastGuidanceReflectionLogId = state.LastGuidanceReflectionLogId,
PendingGuidanceProposal = SanitizeProposalForStatus(state.PendingGuidanceProposal),
Intentions = state.Intentions
.OrderBy(i => i.Status == "active" ? 0 : 1)
.ThenBy(i => i.DueAtUtc ?? DateTime.MaxValue)
.ThenByDescending(i => i.UpdatedAtUtc)
.Take(80)
.ToList(),
CreatedAtUtc = state.CreatedAtUtc,
UpdatedAtUtc = state.UpdatedAtUtc,
LastStepUtc = state.LastStepUtc,
LastStepEffectiveUtc = state.LastStepEffectiveUtc,
NextDueUtc = state.NextDueUtc,
FakeNowUtc = state.FakeNowUtc,
SandboxResetAtUtc = state.SandboxResetAtUtc,
EstimatedInputTokens = state.EstimatedInputTokens,
EstimatedOutputTokens = state.EstimatedOutputTokens,
EstimatedCostCacheMissUsd = state.EstimatedCostCacheMissUsd,
EstimatedCostCacheHitUsd = state.EstimatedCostCacheHitUsd,
EstimatedCostCacheMissTodayUsd = EstimateCacheMissToday(state),
WorldStateMarkdown = FormatWorldStateForPrompt(state.WorldStateMarkdown),
SandboxFiles = state.SandboxFiles.ToList(),
Logs = state.Logs.OrderByDescending(log => log.StartedAtUtc).Take(60).Select(SanitizeLogForStatus).ToList()
};
}
private static KaiAutonomyGuidanceProposal? SanitizeProposalForStatus(KaiAutonomyGuidanceProposal? proposal)
{
if (proposal is null)
return null;
var copy = JsonSerializer.Deserialize<KaiAutonomyGuidanceProposal>(
JsonSerializer.Serialize(proposal, JsonOptions),
JsonOptions);
if (copy is null)
return null;
copy.ChangeSummary = SanitizeAutonomyTextForModel(copy.ChangeSummary);
copy.Reason = SanitizeAutonomyTextForModel(copy.Reason);
copy.ReflectionEntry = SanitizeAutonomyTextForModel(copy.ReflectionEntry);
copy.RawResponsePreview = SanitizeAutonomyTextForModel(copy.RawResponsePreview);
copy.ProposedGuidanceMarkdown = SanitizeAutonomyTextForModel(copy.ProposedGuidanceMarkdown);
return copy;
}
private static void SanitizeProposalForStorage(KaiAutonomyGuidanceProposal proposal)
{
proposal.ChangeSummary = SanitizeAutonomyTextForModel(proposal.ChangeSummary);
proposal.Reason = SanitizeAutonomyTextForModel(proposal.Reason);
proposal.ReflectionEntry = SanitizeAutonomyTextForModel(proposal.ReflectionEntry);
proposal.RawResponsePreview = SanitizeAutonomyTextForModel(proposal.RawResponsePreview);
proposal.ProposedGuidanceMarkdown = SanitizeAutonomyTextForModel(proposal.ProposedGuidanceMarkdown);
}
private static KaiAutonomyLogEntry SanitizeLogForStatus(KaiAutonomyLogEntry log)
{
var copy = JsonSerializer.Deserialize<KaiAutonomyLogEntry>(
JsonSerializer.Serialize(log, JsonOptions),
JsonOptions) ?? new KaiAutonomyLogEntry();
copy.Type = FormatLogTypeForModel(copy.Type);
copy.Reason = SanitizeAutonomyTextForModel(copy.Reason);
copy.Message = SanitizeAutonomyTextForModel(copy.Message);
copy.RawResponsePreview = SanitizeAutonomyTextForModel(copy.RawResponsePreview);
copy.JournalEntry = SanitizeAutonomyTextForModel(copy.JournalEntry);
copy.VisibleMessage = SanitizeAutonomyTextForModel(copy.VisibleMessage);
copy.KaiContinuityNote = SanitizeAutonomyTextForModel(copy.KaiContinuityNote);
copy.HealthAwarenessNote = SanitizeAutonomyTextForModel(copy.HealthAwarenessNote);
copy.NotesForRaymond = SanitizeAutonomyTextForModel(copy.NotesForRaymond);
copy.DeliveryStatus = SanitizeAutonomyTextForModel(copy.DeliveryStatus);
foreach (var op in copy.FileOperations)
op.Message = SanitizeAutonomyTextForModel(op.Message);
foreach (var action in copy.ActionResults)
action.Message = SanitizeAutonomyTextForModel(action.Message);
return copy;
}
private static KaiAutonomyState CloneState(KaiAutonomyState state)
{
var json = JsonSerializer.Serialize(state, JsonOptions);
return JsonSerializer.Deserialize<KaiAutonomyState>(json, JsonOptions) ?? new KaiAutonomyState();
}
private void SetCurrentRun(KaiAutonomyRunStatus status)
{
lock (_runStatusLock)
{
_currentRun = status;
}
}
private KaiAutonomyRunStatus? GetCurrentRun()
{
lock (_runStatusLock)
{
return _currentRun?.Clone();
}
}
private void UpdateCurrentRun(string phase, string message)
{
UpdateCurrentRun(run =>
{
run.Phase = phase;
run.Message = message;
});
}
private void UpdateCurrentRun(Action<KaiAutonomyRunStatus> update)
{
lock (_runStatusLock)
{
if (_currentRun == null)
return;
update(_currentRun);
_currentRun.UpdatedAtUtc = DateTime.UtcNow;
}
}
private void ClearCurrentRun()
{
lock (_runStatusLock)
{
_currentRun = null;
}
}
private static void AddLog(KaiAutonomyState state, KaiAutonomyLogEntry log)
{
state.Logs.Add(log);
TrimLogs(state);
}
private static void TrimLogs(KaiAutonomyState state)
{
if (state.Logs.Count <= MaxLogCount)
return;
state.Logs = state.Logs
.OrderByDescending(log => log.StartedAtUtc)
.Take(MaxLogCount)
.OrderBy(log => log.StartedAtUtc)
.ToList();
}
private static void TrimIntentions(KaiAutonomyState state)
{
if (state.Intentions.Count <= 100)
return;
var active = state.Intentions
.Where(i => i.Status == "active")
.OrderBy(i => i.DueAtUtc ?? DateTime.MaxValue)
.ThenByDescending(i => i.UpdatedAtUtc)
.ToList();
var closed = state.Intentions
.Where(i => i.Status != "active")
.OrderByDescending(i => i.UpdatedAtUtc)
.Take(Math.Max(0, 100 - active.Count))
.ToList();
state.Intentions = active.Concat(closed)
.OrderBy(i => i.Status == "active" ? 0 : 1)
.ThenByDescending(i => i.UpdatedAtUtc)
.ToList();
}
private static decimal EstimateCacheMissToday(KaiAutonomyState state)
{
var today = DateTime.UtcNow.Date;
return state.Logs
.Where(log => log.StartedAtUtc >= today)
.Sum(log => log.EstimatedCostCacheMissUsd);
}
private static decimal EstimateCost(int inputTokens, int outputTokens, bool cacheHit)
{
var inputCost = (inputTokens / 1_000_000m) * (cacheHit ? CacheHitInputCostPerMillion : CacheMissInputCostPerMillion);
var outputCost = (outputTokens / 1_000_000m) * OutputCostPerMillion;
return decimal.Round(inputCost + outputCost, 8);
}
private static int ComputeElapsedMinutes(DateTime? previous, DateTime current)
{
if (previous is null)
return 0;
return (int)Math.Max(0, Math.Round((current - previous.Value).TotalMinutes));
}
private static string ResolveInside(string root, string relativePath)
{
var normalized = NormalizeRelativePath(relativePath);
if (string.IsNullOrWhiteSpace(normalized) || Path.IsPathRooted(normalized))
throw new InvalidOperationException("Relative path is required.");
var fullRoot = Path.GetFullPath(root);
var fullPath = Path.GetFullPath(Path.Combine(fullRoot, normalized.Replace('/', Path.DirectorySeparatorChar)));
var rootWithSeparator = fullRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
if (!fullPath.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"Path escapes root: {relativePath}");
return fullPath;
}
private static string NormalizeRelativePath(string? path)
{
return (path ?? "")
.Replace('\\', '/')
.Trim()
.TrimStart('/')
.Replace("//", "/");
}
private static string Truncate(string text, int maxChars)
{
if (string.IsNullOrEmpty(text) || text.Length <= maxChars)
return text;
return text[..Math.Max(0, maxChars - 18)] + "\n...[truncated]...";
}
private static int Clamp(int value, int min, int max) => Math.Min(max, Math.Max(min, value));
private static decimal ClampDecimal(decimal value, decimal min, decimal max) => Math.Min(max, Math.Max(min, value));
}
public sealed class KaiAutonomyState
{
public KaiAutonomySettings Settings { get; set; } = new();
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime? LastStepUtc { get; set; }
public DateTime? LastStepEffectiveUtc { get; set; }
public DateTime? NextDueUtc { get; set; }
public DateTime? FakeNowUtc { get; set; }
public DateTime? SandboxResetAtUtc { get; set; }
public string WorldStateMarkdown { get; set; } = "";
public string GuidanceMarkdown { get; set; } = "";
public string ReflectionLogMarkdown { get; set; } = "";
public string ActivityLogMarkdown { get; set; } = "";
public DateTime? LastGuidanceReflectionUtc { get; set; }
public string LastGuidanceReflectionLogId { get; set; } = "";
public KaiAutonomyGuidanceProposal? PendingGuidanceProposal { get; set; }
public List<KaiAutonomyIntention> Intentions { get; set; } = new();
public List<string> SandboxFiles { get; set; } = new();
public int EstimatedInputTokens { get; set; }
public int EstimatedOutputTokens { get; set; }
public decimal EstimatedCostCacheMissUsd { get; set; }
public decimal EstimatedCostCacheHitUsd { get; set; }
public List<KaiAutonomyLogEntry> Logs { get; set; } = new();
}
public sealed class KaiAutonomySettings
{
public bool Enabled { get; set; }
public bool ProductionMode { get; set; }
public int IntervalMinutes { get; set; } = 15;
public int MaxOutputTokens { get; set; } = 900;
public decimal DailyBudgetUsd { get; set; }
public bool AllowSandboxFileWrites { get; set; } = true;
public bool AutoGuidanceReflection { get; set; } = true;
public int GuidanceReflectionStepInterval { get; set; } = 12;
public bool EnableMaxVisibleMessagesPerDay { get; set; }
public int MaxVisibleMessagesPerDay { get; set; } = 12;
public bool EnableVisibleMessageCooldown { get; set; }
public int VisibleMessageCooldownMinutes { get; set; } = 60;
public bool EnableQuietHours { get; set; }
public string QuietHoursStart { get; set; } = "22:00";
public string QuietHoursEnd { get; set; } = "08:00";
}
public sealed class KaiAutonomySettingsUpdate
{
public bool? Enabled { get; set; }
public bool? ProductionMode { get; set; }
public int? IntervalMinutes { get; set; }
public int? MaxOutputTokens { get; set; }
public decimal? DailyBudgetUsd { get; set; }
public bool? AllowSandboxFileWrites { get; set; }
public bool? AutoGuidanceReflection { get; set; }
public int? GuidanceReflectionStepInterval { get; set; }
public bool? EnableMaxVisibleMessagesPerDay { get; set; }
public int? MaxVisibleMessagesPerDay { get; set; }
public bool? EnableVisibleMessageCooldown { get; set; }
public int? VisibleMessageCooldownMinutes { get; set; }
public bool? EnableQuietHours { get; set; }
public string? QuietHoursStart { get; set; }
public string? QuietHoursEnd { get; set; }
}
public sealed record KaiAutonomyStepRequest(
int? FakeAdvanceMinutes = null,
string? Reason = null);
public sealed record KaiAutonomyStepResult(
bool Success,
string Message,
KaiAutonomyStatus Status,
KaiAutonomyLogEntry? Log = null);
public sealed record KaiAutonomyGuidanceResult(
bool Success,
string Message,
KaiAutonomyStatus Status,
KaiAutonomyGuidanceProposal? Proposal = null,
KaiAutonomyLogEntry? Log = null);
public sealed class KaiAutonomyGuidanceDecisionRequest
{
public string? ProposalId { get; set; }
}
public sealed class KaiAutonomyGuidanceProposalRequest
{
public string? ProposedGuidanceMarkdown { get; set; }
public string? ChangeSummary { get; set; }
public string? Reason { get; set; }
public string? ReflectionEntry { get; set; }
public string? Source { get; set; }
}
public sealed class KaiAutonomyStatus
{
public KaiAutonomySettings Settings { get; set; } = new();
public bool IsRunning { get; set; }
public KaiAutonomyRunStatus? CurrentRun { get; set; }
public bool SandboxReady { get; set; }
public string TargetConversationId { get; set; } = "";
public string StatePath { get; set; } = "";
public string SandboxRoot { get; set; } = "";
public string AutonomyRoot { get; set; } = "";
public string GuidanceMarkdown { get; set; } = "";
public string ReflectionLogMarkdown { get; set; } = "";
public string ActivityLogMarkdown { get; set; } = "";
public DateTime? LastGuidanceReflectionUtc { get; set; }
public string LastGuidanceReflectionLogId { get; set; } = "";
public KaiAutonomyGuidanceProposal? PendingGuidanceProposal { get; set; }
public List<KaiAutonomyIntention> Intentions { get; set; } = new();
public DateTime CreatedAtUtc { get; set; }
public DateTime UpdatedAtUtc { get; set; }
public DateTime? LastStepUtc { get; set; }
public DateTime? LastStepEffectiveUtc { get; set; }
public DateTime? NextDueUtc { get; set; }
public DateTime? FakeNowUtc { get; set; }
public DateTime? SandboxResetAtUtc { get; set; }
public int EstimatedInputTokens { get; set; }
public int EstimatedOutputTokens { get; set; }
public decimal EstimatedCostCacheMissUsd { get; set; }
public decimal EstimatedCostCacheHitUsd { get; set; }
public decimal EstimatedCostCacheMissTodayUsd { get; set; }
public string WorldStateMarkdown { get; set; } = "";
public List<string> SandboxFiles { get; set; } = new();
public List<KaiAutonomyLogEntry> Logs { get; set; } = new();
}
public sealed class KaiAutonomyPersonaStatus
{
public string Name { get; set; } = "Kai";
public string PersonaKey { get; set; } = "";
public string StatePath { get; set; } = "";
public bool Enabled { get; set; }
public bool ProductionMode { get; set; }
public DateTime? LastStepUtc { get; set; }
public DateTime? LastStepEffectiveUtc { get; set; }
public DateTime? NextDueUtc { get; set; }
public DateTime? FakeNowUtc { get; set; }
public int IntervalMinutes { get; set; }
public decimal DailyBudgetUsd { get; set; }
public DateTime UpdatedAtUtc { get; set; }
public string SandboxRoot { get; set; } = "";
public string AutonomyRoot { get; set; } = "";
public bool SandboxReady { get; set; }
public decimal EstimatedCostCacheMissTodayUsd { get; set; }
public decimal EstimatedCostCacheMissUsd { get; set; }
}
public sealed class KaiAutonomyLogEntry
{
public string Id { get; set; } = Guid.NewGuid().ToString("N")[..8];
public string Type { get; set; } = "";
public string Reason { get; set; } = "";
public bool Success { get; set; }
public string Message { get; set; } = "";
public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime? CompletedAtUtc { get; set; }
public DateTime EffectiveNowUtc { get; set; }
public int ElapsedMinutes { get; set; }
public int InputTokens { get; set; }
public int OutputTokens { get; set; }
public decimal EstimatedCostCacheMissUsd { get; set; }
public decimal EstimatedCostCacheHitUsd { get; set; }
public string RawResponsePreview { get; set; } = "";
public string JournalEntry { get; set; } = "";
public string VisibleMessage { get; set; } = "";
public string KaiContinuityNote { get; set; } = "";
public string HealthAwarenessNote { get; set; } = "";
public string NotesForRaymond { get; set; } = "";
public string GuidanceProposalId { get; set; } = "";
public bool DeliverySucceeded { get; set; }
public string DeliveryStatus { get; set; } = "";
public string DeliveredConversationId { get; set; } = "";
public string DeliveredMessageId { get; set; } = "";
public bool DeliveryTriggeredCompaction { get; set; }
public string DeliveryArchivePath { get; set; } = "";
public List<KaiAutonomyFileOperationResult> FileOperations { get; set; } = new();
public List<KaiAutonomyActionResult> ActionResults { get; set; } = new();
public static KaiAutonomyLogEntry Info(string type, string message, string reason)
{
return new KaiAutonomyLogEntry
{
Type = type,
Reason = reason,
Success = true,
Message = message,
StartedAtUtc = DateTime.UtcNow,
CompletedAtUtc = DateTime.UtcNow,
EffectiveNowUtc = DateTime.UtcNow
};
}
}
public sealed class KaiAutonomyModelResponse
{
public string? WorldStateMarkdown { get; set; }
public string? JournalEntry { get; set; }
public string? VisibleMessage { get; set; }
public string? KaiContinuityNote { get; set; }
public string? HealthAwarenessNote { get; set; }
public string? NotesForRaymond { get; set; }
public List<KaiAutonomyAction>? Actions { get; set; } = new();
public List<KaiAutonomyFileOperation>? FileOperations { get; set; } = new();
}
public sealed class KaiAutonomyReflectionResponse
{
public bool ShouldProposeChange { get; set; }
public string? ReflectionEntry { get; set; }
public string? ProposedGuidanceMarkdown { get; set; }
public string? ChangeSummary { get; set; }
public string? Reason { get; set; }
}
public sealed class KaiAutonomyGuidanceProposal
{
public string Id { get; set; } = "";
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public string SourceLogId { get; set; } = "";
public string PreviousGuidanceMarkdown { get; set; } = "";
public string ProposedGuidanceMarkdown { get; set; } = "";
public string ChangeSummary { get; set; } = "";
public string Reason { get; set; } = "";
public string ReflectionEntry { get; set; } = "";
public string RawResponsePreview { get; set; } = "";
public int InputTokens { get; set; }
public int OutputTokens { get; set; }
public decimal EstimatedCostCacheMissUsd { get; set; }
public decimal EstimatedCostCacheHitUsd { get; set; }
}
public sealed class KaiAutonomyRunStatus
{
public string Id { get; set; } = "";
public string Type { get; set; } = "";
public string Phase { get; set; } = "";
public string Message { get; set; } = "";
public DateTime StartedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime EffectiveNowUtc { get; set; }
public int ElapsedMinutes { get; set; }
public int InputTokens { get; set; }
public int OutputTokens { get; set; }
public string VisibleMessage { get; set; } = "";
public string HealthAwarenessNote { get; set; } = "";
public string GuidanceProposalId { get; set; } = "";
public string GuidanceProposalSummary { get; set; } = "";
public int FileOperationCount { get; set; }
public int AppliedFileOperationCount { get; set; }
public int ActionCount { get; set; }
public int AppliedActionCount { get; set; }
public bool DeliverySucceeded { get; set; }
public string DeliveryStatus { get; set; } = "";
public string DeliveredConversationId { get; set; } = "";
public string DeliveredMessageId { get; set; } = "";
public bool DeliveryTriggeredCompaction { get; set; }
public string DeliveryArchivePath { get; set; } = "";
public KaiAutonomyRunStatus Clone()
{
return (KaiAutonomyRunStatus)MemberwiseClone();
}
}
public sealed class KaiAutonomyIntention
{
public string Id { get; set; } = Guid.NewGuid().ToString("N")[..8];
public string Title { get; set; } = "";
public string Details { get; set; } = "";
public string Status { get; set; } = "active";
public string Priority { get; set; } = "normal";
public string CreatedByLogId { get; set; } = "";
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
public DateTime? DueAtUtc { get; set; }
public DateTime? CompletedAtUtc { get; set; }
public string LastReason { get; set; } = "";
}
public sealed class KaiAutonomyAction
{
public string? Kind { get; set; }
public string? Id { get; set; }
public string? Title { get; set; }
public string? Details { get; set; }
public string? Priority { get; set; }
public int? DueInMinutes { get; set; }
public string? Reason { get; set; }
}
public sealed class KaiAutonomyActionResult
{
public string Kind { get; set; } = "";
public string Id { get; set; } = "";
public bool Applied { get; set; }
public string Message { get; set; } = "";
public static KaiAutonomyActionResult Skipped(string kind, string id, string message)
{
return new KaiAutonomyActionResult
{
Kind = kind,
Id = id,
Applied = false,
Message = message
};
}
}
public sealed class KaiAutonomyFileOperation
{
public string? Path { get; set; }
public string? Action { get; set; }
public string? Content { get; set; }
public string? Reason { get; set; }
}
public sealed class KaiAutonomyFileOperationResult
{
public string Path { get; set; } = "";
public string Action { get; set; } = "";
public bool Applied { get; set; }
public string Message { get; set; } = "";
public static KaiAutonomyFileOperationResult Skipped(string path, string action, string message)
{
return new KaiAutonomyFileOperationResult
{
Path = path,
Action = action,
Applied = false,
Message = message
};
}
}