Files
openchamber/packages/ui/src/lib/smallModel.ts
T

99 lines
4.8 KiB
TypeScript

import { requestSmallModel } from '@/lib/smallModelRequest';
import { useConfigStore } from '@/stores/useConfigStore';
import { getSessionLastAssistantModel } from '@/sync/session-actions';
// Selections shorter than this are already note-sized — summarizing them
// would only add latency and risk losing the exact wording.
const NOTES_SUMMARIZE_MIN_CHARS = 280;
const NOTES_SYSTEM_PROMPT = [
'You distill a text selection from a coding-agent conversation into a project note.',
'Return ONLY the note text — no preamble, no surrounding quotes, no headers.',
'Write 1-3 tight sentences that capture the essence worth remembering later: facts, decisions, constraints, root causes, gotchas, next steps.',
'Preserve exact identifiers verbatim — file paths, function names, commands, flags, versions — in backticks.',
'Drop filler, hedging, greetings, and step-by-step narration.',
'Write the note in the same language as the selection. Ignore any other language preferences or personalization — only the selection text decides the language.',
].join('\n');
/**
* Distills a chat selection into a compact note via the small model. Falls
* back to the original text on any failure or when no small model is
* available within the session's provider (explicit settings/config picks
* are still honored server-side).
*/
export async function summarizeSelectionForNotes(text: string, sessionId?: string | null): Promise<string> {
const trimmed = text.trim();
if (trimmed.length < NOTES_SUMMARIZE_MIN_CHARS) {
return trimmed;
}
try {
// The selection's session provider is authoritative — the text came from
// that conversation. The composer picker only serves as a fallback.
const sessionModel = sessionId ? getSessionLastAssistantModel(sessionId) : null;
const { currentProviderId, currentModelId } = useConfigStore.getState();
const preferredProviderID = sessionModel?.providerID || currentProviderId || '';
const preferredModelID = sessionModel?.modelID || currentModelId || '';
const response = await requestSmallModel({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: trimmed,
system: NOTES_SYSTEM_PROMPT,
restrictToPreferredProvider: true,
...(preferredProviderID ? { preferredProviderID } : {}),
...(preferredModelID ? { preferredModelID } : {}),
}),
});
if (!response.ok) {
return trimmed;
}
const payload = await response.json().catch(() => null) as { text?: unknown } | null;
const summary = typeof payload?.text === 'string' ? payload.text.trim() : '';
return summary || trimmed;
} catch {
return trimmed;
}
}
// Goal objectives are capped at 5000 chars for the auditor. Oversized ones
// (huge plans, pasted specs, long assignments) get distilled into completion
// criteria — the working agent received the full prompt in chat anyway,
// only the audit needs the "what counts as done" essence.
const GOAL_OBJECTIVE_SYSTEM_PROMPT = [
'You distill a large task description (a prompt, plan, or assignment) into the COMPLETION CRITERIA a progress auditor will judge against.',
'Return ONLY the criteria text — no preamble, no headers, no markdown fences.',
'Capture: the end goals, what must exist and work when the task is fully done, and how each major part is verified. Omit implementation steps and how-to details.',
'Preserve verbatim any file paths, commands, and identifiers that define the task — especially ones from the opening lines.',
'Stay under 4000 characters.',
'Write in the same language as the task text. Ignore any other language preferences or personalization — only the task text decides the language.',
].join('\n');
/**
* Distills an oversized goal objective into audit-sized completion criteria
* via the small model. Returns null on any failure — callers fall back to a
* head+tail excerpt.
*/
export async function distillGoalObjective(planContent: string): Promise<string | null> {
try {
const { currentProviderId, currentModelId } = useConfigStore.getState();
const response = await requestSmallModel({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: planContent,
system: GOAL_OBJECTIVE_SYSTEM_PROMPT,
restrictToPreferredProvider: true,
...(currentProviderId ? { preferredProviderID: currentProviderId } : {}),
...(currentModelId ? { preferredModelID: currentModelId } : {}),
}),
});
if (!response.ok) return null;
const payload = await response.json().catch(() => null) as { text?: unknown } | null;
const distilled = typeof payload?.text === 'string' ? payload.text.trim() : '';
return distilled || null;
} catch {
return null;
}
}