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:
+368
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* Per-chat generation session — source of truth for Generate params, board, LoRAs.
|
||||
* Sparse model deltas merge into the active session; buttons always generate from it.
|
||||
*/
|
||||
|
||||
const MAX_DATA_URL_CHARS = 350_000;
|
||||
|
||||
const GEN_KEYS = [
|
||||
'prompt', 'negative', 'width', 'height', 'aspect', 'steps', 'cfg', 'sigma_shift',
|
||||
'seed', 'sampler', 'scheduler', 'batch', 'checkpoint', 'loras', 'controls',
|
||||
'use_init_image', 'clear_init_image', 'init_creativity', 'denoise',
|
||||
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
||||
];
|
||||
|
||||
export function emptySession() {
|
||||
return {
|
||||
gen: {
|
||||
prompt: '',
|
||||
negative: '',
|
||||
width: null,
|
||||
height: null,
|
||||
aspect: null,
|
||||
steps: null,
|
||||
cfg: null,
|
||||
sigma_shift: null,
|
||||
seed: null,
|
||||
sampler: null,
|
||||
scheduler: null,
|
||||
batch: null,
|
||||
checkpoint: null,
|
||||
loras: [],
|
||||
controls: {},
|
||||
use_init_image: false,
|
||||
clear_init_image: false,
|
||||
init_creativity: null,
|
||||
denoise: null,
|
||||
use_mask_image: false,
|
||||
clear_mask_image: false,
|
||||
mask_blur: null,
|
||||
mask_grow: null,
|
||||
},
|
||||
board: {
|
||||
slots: [],
|
||||
selectedSlotId: 'ref1',
|
||||
genResults: [],
|
||||
selectedGenResultId: null,
|
||||
refSeq: 1,
|
||||
},
|
||||
persona: 'neutral',
|
||||
pack: 'ordinary',
|
||||
context_memory: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize boolean generate + legacy actions:["generate"]. */
|
||||
export function normalizeDelta(raw) {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const delta = { ...raw };
|
||||
const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
|
||||
if (delta.generate === true || acts.includes('generate')) {
|
||||
delta.generate = true;
|
||||
}
|
||||
if (typeof delta.ask === 'string') {
|
||||
delta.ask = [delta.ask];
|
||||
}
|
||||
if (!Array.isArray(delta.ask)) {
|
||||
delete delta.ask;
|
||||
} else {
|
||||
delta.ask = delta.ask.map(String).filter(Boolean);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
export function patchWantsGenerate(patch) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (patch.generate === true) {
|
||||
return true;
|
||||
}
|
||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||
return acts.includes('generate');
|
||||
}
|
||||
|
||||
export function patchAskList(patch) {
|
||||
const n = normalizeDelta(patch);
|
||||
return Array.isArray(n?.ask) ? n.ask : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge sparse model delta into session.gen (and pack/persona if present).
|
||||
* Does not run Generate — caller decides via patchWantsGenerate + veto.
|
||||
*/
|
||||
export function mergeDelta(session, rawDelta) {
|
||||
const base = session && typeof session === 'object' ? structuredCloneSession(session) : emptySession();
|
||||
const delta = normalizeDelta(rawDelta);
|
||||
if (!delta) {
|
||||
return base;
|
||||
}
|
||||
if (!base.gen) {
|
||||
base.gen = emptySession().gen;
|
||||
}
|
||||
for (const key of GEN_KEYS) {
|
||||
if (delta[key] === undefined || delta[key] === null) {
|
||||
continue;
|
||||
}
|
||||
if (key === 'loras' && Array.isArray(delta.loras)) {
|
||||
base.gen.loras = delta.loras.map((l) => ({
|
||||
name: l?.name || l,
|
||||
weight: l?.weight != null ? Number(l.weight) : 1,
|
||||
triggers: Array.isArray(l?.triggers) ? l.triggers : undefined,
|
||||
trigger_phrase: l?.trigger_phrase || undefined,
|
||||
})).filter((l) => l.name);
|
||||
continue;
|
||||
}
|
||||
if (key === 'controls' && typeof delta.controls === 'object') {
|
||||
base.gen.controls = { ...(base.gen.controls || {}), ...delta.controls };
|
||||
continue;
|
||||
}
|
||||
if (key === 'checkpoint') {
|
||||
base.gen.checkpoint = typeof delta.checkpoint === 'object'
|
||||
? { ...delta.checkpoint }
|
||||
: { name: String(delta.checkpoint) };
|
||||
continue;
|
||||
}
|
||||
base.gen[key] = delta[key];
|
||||
}
|
||||
if (delta.images != null && delta.batch == null) {
|
||||
base.gen.batch = delta.images;
|
||||
}
|
||||
if (delta.pack) {
|
||||
base.pack = String(delta.pack);
|
||||
}
|
||||
if (delta.persona) {
|
||||
base.persona = String(delta.persona);
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function structuredCloneSession(session) {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(session));
|
||||
} catch {
|
||||
return emptySession();
|
||||
}
|
||||
}
|
||||
|
||||
function slimSrc(src) {
|
||||
if (!src || typeof src !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const s = src.trim();
|
||||
if (!s || s.startsWith('#')) {
|
||||
return null;
|
||||
}
|
||||
if (s.startsWith('data:') && s.length > MAX_DATA_URL_CHARS) {
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Snapshot live UI + board into a persistable session object. */
|
||||
export function snapshotFromLive({
|
||||
genFields,
|
||||
board,
|
||||
persona,
|
||||
pack,
|
||||
context_memory,
|
||||
}) {
|
||||
const session = emptySession();
|
||||
if (genFields && typeof genFields === 'object') {
|
||||
for (const key of GEN_KEYS) {
|
||||
if (genFields[key] !== undefined) {
|
||||
session.gen[key] = genFields[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
session.persona = persona || 'neutral';
|
||||
session.pack = pack || 'ordinary';
|
||||
if (context_memory && typeof context_memory === 'object') {
|
||||
session.context_memory = context_memory;
|
||||
}
|
||||
if (board && typeof board === 'object') {
|
||||
session.board = {
|
||||
slots: (board.slots || []).map((s) => ({
|
||||
id: s.id,
|
||||
type: s.type,
|
||||
label: s.label,
|
||||
src: slimSrc(s.src),
|
||||
attach: !!s.attach,
|
||||
note: s.note || null,
|
||||
})),
|
||||
selectedSlotId: board.selectedSlotId || 'ref1',
|
||||
genResults: (board.genResults || []).map((r) => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
src: slimSrc(r.src),
|
||||
patch: r.patch || null,
|
||||
})),
|
||||
selectedGenResultId: board.selectedGenResultId || null,
|
||||
refSeq: board.refSeq || 1,
|
||||
};
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/** Convert legacy flat chat.params into session shape. */
|
||||
export function sessionFromLegacyParams(params) {
|
||||
if (!params || typeof params !== 'object') {
|
||||
return emptySession();
|
||||
}
|
||||
if (params.gen && typeof params.gen === 'object') {
|
||||
const s = emptySession();
|
||||
s.gen = { ...s.gen, ...params.gen };
|
||||
if (params.board && typeof params.board === 'object') {
|
||||
s.board = { ...s.board, ...params.board };
|
||||
}
|
||||
s.persona = params.persona || s.persona;
|
||||
s.pack = params.pack || s.pack;
|
||||
if (params.context_memory && typeof params.context_memory === 'object') {
|
||||
s.context_memory = params.context_memory;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
const s = emptySession();
|
||||
for (const key of GEN_KEYS) {
|
||||
if (params[key] !== undefined && params[key] !== null) {
|
||||
s.gen[key] = params[key];
|
||||
}
|
||||
}
|
||||
if (Array.isArray(params.loras)) {
|
||||
s.gen.loras = params.loras;
|
||||
}
|
||||
if (params.checkpoint) {
|
||||
s.gen.checkpoint = typeof params.checkpoint === 'object'
|
||||
? params.checkpoint
|
||||
: { name: String(params.checkpoint) };
|
||||
}
|
||||
s.persona = params.persona || 'neutral';
|
||||
s.pack = params.pack || 'ordinary';
|
||||
if (params.context_memory && typeof params.context_memory === 'object') {
|
||||
s.context_memory = params.context_memory;
|
||||
}
|
||||
s.board.genResults = Array.isArray(params.genResults) ? params.genResults : [];
|
||||
s.board.selectedGenResultId = params.selectedGenResultId || null;
|
||||
if (Array.isArray(params.slots)) {
|
||||
s.board.slots = params.slots;
|
||||
}
|
||||
if (params.selectedSlotId) {
|
||||
s.board.selectedSlotId = params.selectedSlotId;
|
||||
}
|
||||
if (params.refSeq) {
|
||||
s.board.refSeq = params.refSeq;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Persist blob for AssistentSaveChat.params */
|
||||
export function toPersistParams(session) {
|
||||
const s = session && typeof session === 'object' ? session : emptySession();
|
||||
const out = {
|
||||
gen: s.gen || emptySession().gen,
|
||||
board: s.board || emptySession().board,
|
||||
persona: s.persona || 'neutral',
|
||||
pack: s.pack || 'ordinary',
|
||||
};
|
||||
if (s.context_memory && typeof s.context_memory === 'object') {
|
||||
out.context_memory = s.context_memory;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function slimText(t, max) {
|
||||
const s = String(t || '');
|
||||
if (s.length <= max) {
|
||||
return s;
|
||||
}
|
||||
return `${s.slice(0, max)}…`;
|
||||
}
|
||||
|
||||
/** Compact context for every LLM turn. */
|
||||
export function compactContext(session, extras = {}) {
|
||||
const s = session && typeof session === 'object' ? session : emptySession();
|
||||
const g = s.gen || {};
|
||||
const board = s.board || {};
|
||||
const slots = board.slots || [];
|
||||
const genSlot = slots.find((x) => x.type === 'generate' || x.id === 'generate');
|
||||
const refs = slots.filter((x) => x.type === 'ref' || String(x.id || '').startsWith('ref'));
|
||||
return {
|
||||
session: true,
|
||||
prompt: slimText(g.prompt, extras.promptMax || 2000),
|
||||
negative: slimText(g.negative, 500),
|
||||
aspect: g.aspect || null,
|
||||
width: g.width ?? null,
|
||||
height: g.height ?? null,
|
||||
steps: g.steps ?? null,
|
||||
cfg: g.cfg ?? null,
|
||||
seed: g.seed ?? null,
|
||||
sigma_shift: g.sigma_shift ?? null,
|
||||
sampler: g.sampler || null,
|
||||
scheduler: g.scheduler || null,
|
||||
batch: g.batch ?? null,
|
||||
checkpoint: g.checkpoint?.name || g.checkpoint || null,
|
||||
selected_loras: (g.loras || []).map((l) => ({
|
||||
name: l.name || l,
|
||||
weight: l.weight != null ? l.weight : 1,
|
||||
})),
|
||||
persona: s.persona || 'neutral',
|
||||
pack: s.pack || 'ordinary',
|
||||
board: {
|
||||
has_generate: !!(genSlot?.src || (board.genResults || []).some((r) => r.src)),
|
||||
refs: refs.map((r) => ({ id: r.id, has_image: !!r.src, attach: !!r.attach })),
|
||||
gen_results: (board.genResults || []).map((r) => ({
|
||||
id: r.id,
|
||||
label: r.label,
|
||||
has_image: !!r.src,
|
||||
selected: r.id === board.selectedGenResultId,
|
||||
})),
|
||||
selected_slot: board.selectedSlotId || null,
|
||||
},
|
||||
architecture_ok: extras.architecture_ok !== false,
|
||||
...extras.extra,
|
||||
};
|
||||
}
|
||||
|
||||
/** Full dump for ask:settings hop. */
|
||||
export function fullSettingsDump(session, extras = {}) {
|
||||
const compact = compactContext(session, extras);
|
||||
const s = session && typeof session === 'object' ? session : emptySession();
|
||||
return {
|
||||
...compact,
|
||||
detail: 'settings',
|
||||
gen: { ...(s.gen || {}) },
|
||||
controls: s.gen?.controls || {},
|
||||
exact: extras.exact || null,
|
||||
krea_profiles: extras.kreaProfiles || null,
|
||||
session_exact: extras.sessionExact || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
|
||||
const delta = normalizeDelta(patch) || {};
|
||||
const vetoed = typeof vetoFn === 'function' ? !!vetoFn(userText) : false;
|
||||
const generate = !vetoed && patchWantsGenerate(delta);
|
||||
const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
|
||||
const look = !!(hasLook && !generate && !vetoed);
|
||||
const ask = patchAskList(delta);
|
||||
return { generate, look, vetoed, ask };
|
||||
}
|
||||
|
||||
export function attachSession(SA) {
|
||||
SA.session = {
|
||||
emptySession,
|
||||
normalizeDelta,
|
||||
mergeDelta,
|
||||
patchWantsGenerate,
|
||||
patchAskList,
|
||||
snapshotFromLive,
|
||||
sessionFromLegacyParams,
|
||||
toPersistParams,
|
||||
compactContext,
|
||||
fullSettingsDump,
|
||||
resolveTurnIntent,
|
||||
GEN_KEYS,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user