99 lines
4.8 KiB
TypeScript
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;
|
|
}
|
|
}
|