Ship Assistent 0.14.0: chat sessions, ask-only hops, and context compression.
Per-chat Generate session with sparse deltas; drop Cards/Civitai/wanted hops; rolling history summary via the same Ollama model with a budget chip and /compress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+65
-119
@@ -46,28 +46,25 @@ public partial class SwarmAssistentExtension
|
||||
patch["look_at"] = patch["vision_slots"];
|
||||
}
|
||||
}
|
||||
if (ActionsContain(patch, "generate"))
|
||||
{
|
||||
patch["generate"] = true;
|
||||
}
|
||||
if (patch["ask"] is JValue askVal && askVal.Type == JTokenType.String)
|
||||
{
|
||||
string one = askVal.ToString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(one))
|
||||
{
|
||||
patch["ask"] = new JArray(one);
|
||||
}
|
||||
else
|
||||
{
|
||||
patch.Remove("ask");
|
||||
}
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
static bool LooksLikeCardObject(JObject obj)
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool cardish = HasValue(obj, "kind") || HasValue(obj, "triggers") || HasValue(obj, "when") || HasValue(obj, "prompt_hint");
|
||||
bool genish = HasValue(obj, "prompt") || HasValue(obj, "negative") || HasValue(obj, "loras") || HasValue(obj, "actions")
|
||||
|| HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "steps") || HasValue(obj, "cfg")
|
||||
|| HasValue(obj, "aspect") || HasValue(obj, "seed") || HasValue(obj, "search_query")
|
||||
|| HasValue(obj, "civitai_query") || HasValue(obj, "look_at") || HasValue(obj, "controls");
|
||||
if (cardish && !genish && (HasValue(obj, "name") || HasValue(obj, "triggers") || HasValue(obj, "when")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return HasValue(obj, "kind") && HasValue(obj, "name")
|
||||
&& (HasValue(obj, "triggers") || HasValue(obj, "when") || HasValue(obj, "prompt_hint") || HasValue(obj, "notes"));
|
||||
}
|
||||
|
||||
JObject TryParsePatch(string reply)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(reply))
|
||||
@@ -82,7 +79,7 @@ public partial class SwarmAssistentExtension
|
||||
try
|
||||
{
|
||||
JObject obj = JObject.Parse(raw);
|
||||
if (obj is null || LooksLikeCardObject(obj))
|
||||
if (obj is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -90,7 +87,7 @@ public partial class SwarmAssistentExtension
|
||||
{
|
||||
JObject normalized = NormalizePatch(obj);
|
||||
lastAny = normalized;
|
||||
if (FenceIsTerminalPatch(obj))
|
||||
if (FenceIsTerminalPatch(normalized))
|
||||
{
|
||||
lastTerminal = normalized;
|
||||
}
|
||||
@@ -105,10 +102,9 @@ public partial class SwarmAssistentExtension
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the reply already contains a closed fenced patch/card that is "done enough" to act on,
|
||||
/// If the reply already contains a closed fenced patch that is "done enough" to act on,
|
||||
/// cut everything after it. Do NOT stop on weak fences (pack/creativity/notes-only) — models
|
||||
/// often emit a tiny JSON first then the real prompt fence; aborting early cuts the prompt
|
||||
/// and blocks skill_load / generate.
|
||||
/// often emit a tiny JSON first then the real prompt fence; aborting early cuts the prompt.
|
||||
/// </summary>
|
||||
static bool TryTruncateAtCompleteFence(string reply, out string truncated)
|
||||
{
|
||||
@@ -128,7 +124,7 @@ public partial class SwarmAssistentExtension
|
||||
string raw = match.Groups[1].Value.Trim();
|
||||
try
|
||||
{
|
||||
JObject obj = JObject.Parse(raw);
|
||||
JObject obj = NormalizePatch(JObject.Parse(raw));
|
||||
if (obj is null || !FenceIsTerminalPatch(obj))
|
||||
{
|
||||
continue;
|
||||
@@ -145,7 +141,7 @@ public partial class SwarmAssistentExtension
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when a closed fence is worth aborting the Ollama stream (real deliverable or tool hop).
|
||||
/// True when a closed fence is worth aborting the Ollama stream (real deliverable or ask hop).
|
||||
/// </summary>
|
||||
static bool FenceIsTerminalPatch(JObject obj)
|
||||
{
|
||||
@@ -153,7 +149,11 @@ public partial class SwarmAssistentExtension
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (LooksLikeCardObject(obj))
|
||||
if (obj["generate"]?.Type == JTokenType.Boolean && obj["generate"].Value<bool>())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (HasAsk(obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -165,50 +165,18 @@ public partial class SwarmAssistentExtension
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (HasValue(obj, "search_query") || HasValue(obj, "civitai_query"))
|
||||
if (ActionsContain(obj, "generate"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (HasValue(obj, "memory_query") || HasValue(obj, "tag_query") || HasValue(obj, "inventory_query"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (obj["actions"] is JArray acts)
|
||||
{
|
||||
foreach (JToken a in acts)
|
||||
{
|
||||
string s = a?.ToString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (s.Equals("skill_load", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("persona_read", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("memory_get", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("memory_search", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("heard_search", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("lookup_tags", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("list_inventory", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("search_civitai", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("interrupt", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("generate", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("memory_upsert", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.Equals("user_pref_upsert", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
string prompt = obj["prompt"]?.ToString()?.Trim() ?? "";
|
||||
if (prompt.Length >= 48)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Real param change without prose notes
|
||||
if (HasValue(obj, "loras") || HasValue(obj, "aspect") || HasValue(obj, "steps")
|
||||
|| HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "cfg")
|
||||
|| HasValue(obj, "seed") || HasValue(obj, "controls")
|
||||
|| HasValue(obj, "memories") || HasValue(obj, "user_prefs"))
|
||||
|| HasValue(obj, "seed") || HasValue(obj, "controls"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -216,14 +184,40 @@ public partial class SwarmAssistentExtension
|
||||
return false;
|
||||
}
|
||||
|
||||
static string ExtractSearchQuery(JObject patch)
|
||||
static bool HasAsk(JObject patch)
|
||||
{
|
||||
if (patch is null)
|
||||
if (patch?["ask"] is JArray asks)
|
||||
{
|
||||
return null;
|
||||
foreach (JToken t in asks)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(t?.ToString()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
string q = (patch["search_query"] ?? patch["civitai_query"])?.ToString()?.Trim();
|
||||
return string.IsNullOrWhiteSpace(q) ? null : q;
|
||||
return !string.IsNullOrWhiteSpace(patch?["ask"]?.ToString());
|
||||
}
|
||||
|
||||
static bool AskContains(JObject patch, string name)
|
||||
{
|
||||
if (patch is null || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (patch["ask"] is JArray asks)
|
||||
{
|
||||
foreach (JToken t in asks)
|
||||
{
|
||||
if (string.Equals(t?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return string.Equals(patch["ask"]?.ToString()?.Trim(), name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
static bool ActionsContain(JObject patch, string action)
|
||||
@@ -242,26 +236,7 @@ public partial class SwarmAssistentExtension
|
||||
return false;
|
||||
}
|
||||
|
||||
static string ExtractMemoryQuery(JObject patch)
|
||||
{
|
||||
string q = patch?["memory_query"]?.ToString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
return q;
|
||||
}
|
||||
return ActionsContain(patch, "memory_search") ? ExtractSearchQuery(patch) : null;
|
||||
}
|
||||
|
||||
static string ExtractTagQuery(JObject patch)
|
||||
{
|
||||
string q = patch?["tag_query"]?.ToString()?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
return q;
|
||||
}
|
||||
return ActionsContain(patch, "lookup_tags") ? ExtractSearchQuery(patch) : null;
|
||||
}
|
||||
|
||||
/// <summary>Server tool hops are ask-only: settings dump or truncated inventory.</summary>
|
||||
static string NextToolHop(JObject patch, HashSet<string> skip = null)
|
||||
{
|
||||
if (patch is null)
|
||||
@@ -269,42 +244,13 @@ public partial class SwarmAssistentExtension
|
||||
return null;
|
||||
}
|
||||
bool Skip(string tool) => skip is not null && skip.Contains(tool);
|
||||
if (ActionsContain(patch, "memory_get") && !Skip("memory_get"))
|
||||
if (AskContains(patch, "settings") && !Skip("ask_settings"))
|
||||
{
|
||||
return "memory_get";
|
||||
return "ask_settings";
|
||||
}
|
||||
if ((ActionsContain(patch, "memory_search") || !string.IsNullOrWhiteSpace(patch["memory_query"]?.ToString()))
|
||||
&& !Skip("memory_search"))
|
||||
if (AskContains(patch, "inventory") && !Skip("ask_inventory"))
|
||||
{
|
||||
return "memory_search";
|
||||
}
|
||||
if ((ActionsContain(patch, "heard_search") || string.Equals(patch["heard_query"]?.ToString(), "1", StringComparison.Ordinal))
|
||||
&& !Skip("heard_search"))
|
||||
{
|
||||
return "heard_search";
|
||||
}
|
||||
if ((ActionsContain(patch, "lookup_tags") || !string.IsNullOrWhiteSpace(patch["tag_query"]?.ToString()))
|
||||
&& !Skip("lookup_tags"))
|
||||
{
|
||||
return "lookup_tags";
|
||||
}
|
||||
if ((ActionsContain(patch, "list_inventory") || !string.IsNullOrWhiteSpace(patch["inventory_query"]?.ToString()))
|
||||
&& !Skip("list_inventory"))
|
||||
{
|
||||
return "list_inventory";
|
||||
}
|
||||
if (ActionsContain(patch, "skill_load") && !Skip("skill_load"))
|
||||
{
|
||||
return "skill_load";
|
||||
}
|
||||
if (ActionsContain(patch, "persona_read") && !Skip("persona_read"))
|
||||
{
|
||||
return "persona_read";
|
||||
}
|
||||
if ((ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch)))
|
||||
&& !Skip("civitai"))
|
||||
{
|
||||
return "civitai";
|
||||
return "ask_inventory";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user