Skip to content

Commit 68d98e0

Browse files
feat: proactive skill suggestions via dream skill gap detection (#55)
Adds a new dream pass — skill gap detection — that reviews the accumulated conversation log for recurring request patterns and saves new skills directly to the skill store before the existing skill consolidation pass runs (which deduplicates them automatically). - DreamOptions: SkillGapEnabled (default true) + SkillGapDirectivePath - DreamService: RunSkillGapDetectionPassAsync runs between memory consolidation and skill consolidation; loads directive from file or built-in fallback; groups log entries by session; lists existing skills so the LLM avoids duplicates; parses { "toSave": [...] } JSON - BuiltInSkillGapDirective enforces 2-occurrence threshold, name/ summary/content format rules, and an empty-result contract - SkillGapResultDto / SkillGapEntryDto DTOs alongside existing DTOs Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent ae57232 commit 68d98e0

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

src/RockBot.Host.Abstractions/DreamOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,13 @@ public sealed class DreamOptions
4242
/// When the file does not exist, a built-in fallback directive is used.
4343
/// </summary>
4444
public string PreferenceDirectivePath { get; set; } = "pref-dream.md";
45+
46+
/// <summary>Whether the skill gap detection pass is enabled.</summary>
47+
public bool SkillGapEnabled { get; set; } = true;
48+
49+
/// <summary>
50+
/// Path to the skill gap detection directive file, relative to <see cref="AgentProfileOptions.BasePath"/>.
51+
/// When the file does not exist, a built-in fallback directive is used.
52+
/// </summary>
53+
public string SkillGapDirectivePath { get; set; } = "skill-gap.md";
4554
}

src/RockBot.Host/DreamService.cs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal sealed class DreamService : IHostedService, IDisposable
3333
private string? _skillDreamDirective;
3434
private string? _skillOptimizeDirective;
3535
private string? _prefDreamDirective;
36+
private string? _skillGapDirective;
3637

3738
public DreamService(
3839
ILongTermMemory memory,
@@ -120,6 +121,19 @@ public Task StartAsync(CancellationToken cancellationToken)
120121
_logger.LogInformation("DreamService: loaded pref directive from {Path}", prefDirectivePath);
121122
}
122123

124+
if (_skillStore is not null && _conversationLog is not null)
125+
{
126+
var skillGapDirectivePath = ResolvePath(_options.SkillGapDirectivePath, _profileOptions.BasePath);
127+
_skillGapDirective = File.Exists(skillGapDirectivePath)
128+
? File.ReadAllText(skillGapDirectivePath)
129+
: BuiltInSkillGapDirective;
130+
131+
if (!File.Exists(skillGapDirectivePath))
132+
_logger.LogWarning("DreamService: skill gap directive not found at {Path}; using built-in fallback", skillGapDirectivePath);
133+
else
134+
_logger.LogInformation("DreamService: loaded skill gap directive from {Path}", skillGapDirectivePath);
135+
}
136+
123137
_timer = new Timer(
124138
state => { _ = DreamAsync(); },
125139
null,
@@ -280,6 +294,9 @@ private async Task DreamAsync()
280294
entry.Id, entry.Category ?? "(none)", entry.Content);
281295
}
282296

297+
if (_skillStore is not null)
298+
await RunSkillGapDetectionPassAsync();
299+
283300
if (_skillStore is not null)
284301
await ConsolidateSkillsAsync();
285302

@@ -682,6 +699,97 @@ private async Task OptimizeSkillsAsync()
682699
deleted, saved);
683700
}
684701

702+
/// <summary>
703+
/// Scans the conversation log for recurring request patterns that would benefit
704+
/// from a reusable skill and saves any discovered skills directly to the skill store.
705+
/// Runs before skill consolidation so that the consolidation pass can deduplicate
706+
/// any new skills alongside existing ones.
707+
/// </summary>
708+
private async Task RunSkillGapDetectionPassAsync()
709+
{
710+
if (_conversationLog is null || _skillStore is null || !_options.SkillGapEnabled)
711+
return;
712+
713+
var entries = await _conversationLog.ReadAllAsync();
714+
if (entries.Count == 0)
715+
{
716+
_logger.LogDebug("DreamService: skill gap detection — no log entries; skipping");
717+
return;
718+
}
719+
720+
var existingSkills = await _skillStore.ListAsync();
721+
722+
_logger.LogInformation(
723+
"DreamService: skill gap detection pass — {EntryCount} log entries, {SkillCount} existing skills",
724+
entries.Count, existingSkills.Count);
725+
726+
// Build user message: turns grouped by session + existing skill catalog
727+
var userMessage = new StringBuilder();
728+
userMessage.AppendLine("Review the following conversation log for recurring request patterns:");
729+
userMessage.AppendLine();
730+
731+
var bySession = entries
732+
.GroupBy(e => e.SessionId)
733+
.ToDictionary(g => g.Key, g => g.ToList());
734+
735+
foreach (var (sessionId, sessionEntries) in bySession)
736+
{
737+
userMessage.AppendLine($"## Session: {sessionId}");
738+
foreach (var e in sessionEntries)
739+
userMessage.AppendLine($"[{e.Role}] {e.Content}");
740+
userMessage.AppendLine();
741+
}
742+
743+
if (existingSkills.Count > 0)
744+
{
745+
userMessage.AppendLine("## Existing skills (do not duplicate these):");
746+
foreach (var s in existingSkills)
747+
userMessage.AppendLine($"- {s.Name}: {s.Summary}");
748+
userMessage.AppendLine();
749+
}
750+
751+
var messages = new List<ChatMessage>
752+
{
753+
new(ChatRole.System, _skillGapDirective ?? BuiltInSkillGapDirective),
754+
new(ChatRole.User, userMessage.ToString())
755+
};
756+
757+
var response = await _llmClient.GetResponseAsync(messages, new ChatOptions());
758+
var raw = response.Text?.Trim() ?? string.Empty;
759+
var json = ExtractJsonObject(raw);
760+
761+
if (string.IsNullOrEmpty(json))
762+
{
763+
_logger.LogWarning("DreamService: skill gap LLM returned no parseable JSON; skipping");
764+
return;
765+
}
766+
767+
_logger.LogDebug("DreamService: skill gap JSON ({Length} chars): {Json}", json.Length, json);
768+
769+
var result = JsonSerializer.Deserialize<SkillGapResultDto>(json, JsonOptions);
770+
var saved = 0;
771+
772+
foreach (var dto in result?.ToSave ?? [])
773+
{
774+
if (string.IsNullOrWhiteSpace(dto.Name) || string.IsNullOrWhiteSpace(dto.Content))
775+
continue;
776+
777+
var skill = new Skill(
778+
Name: dto.Name.Trim(),
779+
Summary: dto.Summary?.Trim() ?? string.Empty,
780+
Content: dto.Content.Trim(),
781+
CreatedAt: DateTimeOffset.UtcNow,
782+
UpdatedAt: DateTimeOffset.UtcNow,
783+
LastUsedAt: null);
784+
785+
await _skillStore.SaveAsync(skill);
786+
saved++;
787+
_logger.LogDebug("DreamService: skill gap created new skill '{Name}'", skill.Name);
788+
}
789+
790+
_logger.LogInformation("DreamService: skill gap detection complete — {Saved} new skill(s) created", saved);
791+
}
792+
685793
/// <summary>
686794
/// Analyzes the accumulated conversation log for durable user preference patterns
687795
/// and saves inferred preferences as tagged memory entries.
@@ -866,6 +974,27 @@ Only improve skills where the failure is clearly addressable by better instructi
866974
If no improvements are warranted, return: { "toDelete": [], "toSave": [] }
867975
""";
868976

977+
private const string BuiltInSkillGapDirective = """
978+
You are a skill gap detection assistant. Review the conversation log for recurring request
979+
patterns that would benefit from a reusable skill.
980+
981+
Only suggest a new skill when the same type of request appears 2 or more times across
982+
different sessions, or with clear recurring intent in a single session.
983+
984+
Existing skills are listed below — do not suggest skills already adequately covered by them.
985+
986+
Return ONLY a JSON object:
987+
{ "toSave": [ { "name": "...", "summary": "...", "content": "..." } ] }
988+
989+
Rules:
990+
- name: short, lowercase, hyphen-separated (e.g. "summarize-emails", "daily-standup")
991+
- summary: one sentence, 15 words or fewer
992+
- content: step-by-step instructions the agent should follow when executing this skill
993+
- Only suggest skills with clear, repeatable value across sessions
994+
995+
If no recurring patterns warrant a new skill, return: { "toSave": [] }
996+
""";
997+
869998
private const string BuiltInPrefDirective = """
870999
You are a user preference inference assistant. Review the conversation log for durable, recurring preference patterns.
8711000
Look for: formatting preferences, comment style, tool corrections, topic clusters, and communication style signals.
@@ -913,4 +1042,8 @@ private sealed record PrefEntryDto(
9131042
string? Category,
9141043
IReadOnlyList<string>? Tags,
9151044
IReadOnlyDictionary<string, string>? Metadata);
1045+
1046+
private sealed record SkillGapResultDto(List<SkillGapEntryDto>? ToSave);
1047+
1048+
private sealed record SkillGapEntryDto(string Name, string? Summary, string Content);
9161049
}

0 commit comments

Comments
 (0)