Ship Assistent disk persist, park LLM, and memory UI cleanup.

Split the extension into partials, persist chats on the data volume, park/warm the chat model around Generate, and drop dual raw/persona dump paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 01:03:40 +03:00
co-authored by Cursor
parent 880e2dbea2
commit 8702b64e12
12 changed files with 1192 additions and 114 deletions
+152 -30
View File
@@ -13,6 +13,7 @@ namespace Mrleo1nid.SwarmAssistent;
public partial class SwarmAssistentExtension
{
const int MaxCivitaiHopsFallback = 2;
const int MaxToolHopsFallback = 4;
List<JObject> BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null, IEnumerable<string> skillIds = null)
{
@@ -139,12 +140,12 @@ public partial class SwarmAssistentExtension
Logs.Debug($"Assistent memory seed: {ex.Message}");
}
string retrieveQuery = BuildRetrieveQuery(userMessages, contextJson);
string retrieveQuery = BuildRetrieveQuery(userMessages, contextJson, packName);
JArray hits = [];
try
{
int topK = Config.LoadAssistant(pid)["memory_top_k"]?.Value<int?>() ?? 10;
hits = await Memory.RetrieveAsync(root, retrieveQuery, topK, embed, Config.PersonaExtendsChain(pid));
AssistentMemory.RetrieveOptions opt = MemoryRetrieveOptions(pid);
hits = await Memory.RetrieveAsync(root, retrieveQuery, opt.TopK, embed, Config.PersonaExtendsChain(pid), opt);
}
catch (Exception ex)
{
@@ -156,7 +157,9 @@ public partial class SwarmAssistentExtension
JArray civitaiResults = [];
string reply = "";
JObject lastRaw = null;
int maxHops = CfgInt("max_civitai_hops", MaxCivitaiHopsFallback);
int maxHops = Math.Max(CfgInt("max_civitai_hops", MaxCivitaiHopsFallback), CfgInt("max_tool_hops", MaxToolHopsFallback));
HashSet<string> hopDone = new(StringComparer.OrdinalIgnoreCase);
var chain = Config.PersonaExtendsChain(pid);
for (int hop = 0; hop < maxHops; hop++)
{
if (onHopStart is not null)
@@ -166,44 +169,37 @@ public partial class SwarmAssistentExtension
(reply, lastRaw) = await CallOllamaChat(root, modelName, messages, stream: onDelta is not null, onDelta, pid);
JObject patch = TryParsePatch(reply);
await ApplyMemoryActions(root, patch, embed, pid);
if (hop + 1 >= maxHops || !WantsCivitaiSearch(patch))
if (hop + 1 >= maxHops)
{
break;
}
string query = ExtractSearchQuery(patch);
if (string.IsNullOrWhiteSpace(query))
string tool = NextToolHop(patch);
if (string.IsNullOrWhiteSpace(tool))
{
break;
}
JObject search = await AssistentSearchCivitai(session, query, 8);
if (search["error"] is not null)
(string follow, JArray civitaiHop) = await RunToolHop(session, root, embed, pid, chain, patch, tool, hopDone);
if (follow is null)
{
messages.Add(new JObject { ["role"] = "assistant", ["content"] = reply });
messages.Add(new JObject
{
["role"] = "user",
["content"] = $"Civitai search failed: {search["error"]}. Continue without download — use only available_loras from context.",
});
continue;
break;
}
if (civitaiHop is { Count: > 0 })
{
civitaiResults = civitaiHop;
}
civitaiResults = search["results"] as JArray ?? [];
messages.Add(new JObject { ["role"] = "assistant", ["content"] = reply });
messages.Add(new JObject
{
["role"] = "user",
["content"] =
"Civitai search results (JSON). Prefer `krea_likely: true`. Do NOT download yourself — the UI shows Confirm cards. " +
"Pick useful LoRAs from results or available_loras, emit a normal patch (prompt/loras). " +
"Omit search_civitai from actions unless you need a different query.\n```json\n" +
civitaiResults.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
});
messages.Add(new JObject { ["role"] = "user", ["content"] = follow });
}
return (reply, lastRaw, civitaiResults);
}
static string BuildRetrieveQuery(JArray userMessages, string contextJson)
static string BuildRetrieveQuery(JArray userMessages, string contextJson, string packName = null)
{
StringBuilder sb = new();
if (!string.IsNullOrWhiteSpace(packName))
{
sb.Append(packName).Append(' ');
}
if (!string.IsNullOrWhiteSpace(contextJson))
{
try
@@ -216,7 +212,7 @@ public partial class SwarmAssistentExtension
}
if (ctx["enabled_loras"] is JArray en)
{
foreach (JToken t in en.Take(8))
foreach (JToken t in en.Take(12))
{
string n = t?["name"]?.ToString() ?? t?.ToString();
if (!string.IsNullOrWhiteSpace(n))
@@ -229,23 +225,149 @@ public partial class SwarmAssistentExtension
{
sb.Append("krea ").Append(ctx["krea_profile"]).Append(' ');
}
string aspect = ctx["aspect"]?.ToString();
if (!string.IsNullOrWhiteSpace(aspect))
{
sb.Append(aspect).Append(' ');
}
string prompt = ctx["prompt"]?.ToString();
if (!string.IsNullOrWhiteSpace(prompt))
{
sb.Append(prompt.Length > 400 ? prompt[..400] : prompt).Append(' ');
}
}
catch
{
// ignore
}
}
foreach (JToken msg in (userMessages ?? []).Reverse().Take(2))
foreach (JToken msg in (userMessages ?? []).Reverse().Take(3))
{
if (msg is JObject mo && string.Equals(mo["role"]?.ToString(), "user", StringComparison.OrdinalIgnoreCase))
{
sb.Append(mo["content"]?.ToString()).Append(' ');
string c = mo["content"]?.ToString() ?? "";
sb.Append(c.Length > 500 ? c[..500] : c).Append(' ');
}
}
string q = CollapseWs(sb.ToString());
return string.IsNullOrWhiteSpace(q) ? "krea2 prompting" : q;
}
AssistentMemory.RetrieveOptions MemoryRetrieveOptions(string pid)
{
JObject a = Config.LoadAssistant(pid) ?? new JObject();
AssistentMemory.RetrieveOptions opt = new()
{
TopK = a["memory_top_k"]?.Value<int?>() ?? 10,
MinScore = a["memory_min_score"]?.Value<float?>() ?? 0.32f,
ApplyQuotas = true,
};
if (a["memory_quotas"] is JObject quotas)
{
Dictionary<string, int> d = new(StringComparer.OrdinalIgnoreCase);
foreach (JProperty p in quotas.Properties())
{
d[p.Name] = p.Value?.Value<int?>() ?? 2;
}
opt.Quotas = d;
}
return opt;
}
async Task<(string follow, JArray civitai)> RunToolHop(
Session session,
string root,
string embed,
string pid,
IEnumerable<string> chain,
JObject patch,
string tool,
HashSet<string> hopDone)
{
if (tool == "memory_get")
{
JArray got = [];
foreach (JToken t in patch["memories"] as JArray ?? [])
{
if (t is not JObject mo)
{
continue;
}
string kind = mo["kind"]?.ToString() ?? "note";
string key = mo["key"]?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(key))
{
continue;
}
string sig = $"get:{kind}:{key}";
if (!hopDone.Add(sig))
{
continue;
}
JObject row = Memory.Get(kind, key, chain);
got.Add(row ?? new JObject { ["kind"] = kind, ["key"] = key, ["missing"] = true });
}
if (got.Count == 0)
{
return (null, null);
}
return (
"memory_get results (JSON). Use these facts; omit memory_get unless you need a different key.\n```json\n"
+ got.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
null);
}
if (tool == "memory_search")
{
string q = ExtractMemoryQuery(patch);
if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("search:" + q))
{
return (null, null);
}
string kind = patch["memory_kind"]?.ToString();
int topK = Config.LoadAssistant(pid)["memory_top_k"]?.Value<int?>() ?? 10;
JArray rows = await Memory.SearchAsync(root, q, kind, topK, embed, chain);
return (
"memory_search results (JSON, hybrid FTS+vector). Omit memory_search unless you need a different query.\n```json\n"
+ rows.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
null);
}
if (tool == "lookup_tags")
{
string q = ExtractTagQuery(patch);
if (string.IsNullOrWhiteSpace(q) || !hopDone.Add("tags:" + q))
{
return (null, null);
}
int lim = Config.LoadAssistant(pid)["tag_lookup_limit"]?.Value<int?>() ?? 20;
JArray tags = Memory.LookupTags(q, lim);
return (
"lookup_tags results from Danbooru csv (canonical name, aliases, post_count). Krea prompts stay natural prose — use this to check spelling/aliases, do not dump tag soup.\n```json\n"
+ tags.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
null);
}
if (tool == "civitai")
{
string query = ExtractSearchQuery(patch);
if (string.IsNullOrWhiteSpace(query) || !hopDone.Add("civitai:" + query))
{
return (null, null);
}
JObject search = await AssistentSearchCivitai(session, query, 8);
if (search["error"] is not null)
{
return ($"Civitai search failed: {search["error"]}. Continue without download — use only available_loras from context.", null);
}
JArray civitaiResults = search["results"] as JArray ?? [];
return (
"Civitai search results (JSON). Prefer `krea_likely: true`. Do NOT download yourself — the UI shows Confirm cards. " +
"Pick useful LoRAs from results or available_loras, emit a normal patch (prompt/loras). " +
"Omit search_civitai from actions unless you need a different query.\n```json\n" +
civitaiResults.ToString(Newtonsoft.Json.Formatting.None) + "\n```",
civitaiResults);
}
return (null, null);
}
static string InjectMemoryHits(string contextJson, JArray hits, JObject exact = null)
{
JObject ctx;