115 lines
4.1 KiB
JavaScript
115 lines
4.1 KiB
JavaScript
import { GOAL_OBJECTIVE_CHAR_LIMIT, writeObjective } from './objectives.js';
|
|
|
|
const TRIM_MARKER = '\n\n[… objective trimmed for the auditor — the full prompt was delivered in the chat message …]\n\n';
|
|
|
|
export const buildGoalIntroText = (tokenBudget) => {
|
|
const budgetLine = tokenBudget
|
|
? ` A token budget of ${tokenBudget} tokens applies to this goal.`
|
|
: '';
|
|
return '<system-reminder>\n'
|
|
+ 'Goal mode is active for this session. The user message above defines the goal objective. '
|
|
+ 'Work toward it across turns; whenever you stop before the objective is verifiably complete, the system will automatically prompt you to continue. '
|
|
+ 'Progress is evaluated independently after each turn, so end every turn with a clear, factual statement of what is done, what was verified, and what remains.'
|
|
+ budgetLine
|
|
+ '\n</system-reminder>';
|
|
};
|
|
|
|
const fitObjective = async ({ objective, directory, sessionID, providerID, modelID, warn }) => {
|
|
if (objective.length <= GOAL_OBJECTIVE_CHAR_LIMIT) return objective;
|
|
|
|
let distilled = null;
|
|
try {
|
|
const { generateSmallModelText } = await import('../small-model/index.js');
|
|
const generated = await generateSmallModelText({
|
|
restrictToPreferredProvider: true,
|
|
prompt: objective,
|
|
system: [
|
|
'You distill a large task description 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.',
|
|
'Preserve verbatim any file paths, commands, and identifiers that define the task.',
|
|
'Stay under 4000 characters.',
|
|
'Write in the same language as the task text.',
|
|
].join('\n'),
|
|
directory,
|
|
sessionID,
|
|
preferredProviderID: providerID,
|
|
preferredModelID: modelID,
|
|
});
|
|
distilled = typeof generated?.text === 'string' ? generated.text.trim() : null;
|
|
} catch (error) {
|
|
warn('goal objective distillation failed', error);
|
|
}
|
|
|
|
if (distilled) return distilled.slice(0, GOAL_OBJECTIVE_CHAR_LIMIT);
|
|
const half = Math.max(0, Math.floor((GOAL_OBJECTIVE_CHAR_LIMIT - TRIM_MARKER.length) / 2));
|
|
return `${objective.slice(0, half)}${TRIM_MARKER}${objective.slice(-half)}`;
|
|
};
|
|
|
|
export const createSessionGoal = async ({
|
|
baseUrl,
|
|
authHeaders,
|
|
sessionID,
|
|
directory,
|
|
objective,
|
|
tokenBudget = null,
|
|
providerID,
|
|
modelID,
|
|
onWarning,
|
|
}) => {
|
|
const warn = (message, error) => {
|
|
if (typeof onWarning === 'function') {
|
|
onWarning(message, error);
|
|
return;
|
|
}
|
|
console.warn(`[session-goal] ${message}:`, error?.message || error);
|
|
};
|
|
const objectiveText = await fitObjective({
|
|
objective: String(objective ?? '').trim(),
|
|
directory,
|
|
sessionID,
|
|
providerID,
|
|
modelID,
|
|
warn,
|
|
});
|
|
if (!objectiveText) throw new Error('goal objective is required');
|
|
|
|
let objectiveFile = false;
|
|
try {
|
|
await writeObjective(sessionID, objectiveText);
|
|
objectiveFile = true;
|
|
} catch (error) {
|
|
warn('goal objective file write failed, falling back to inline', error);
|
|
}
|
|
|
|
const now = Date.now();
|
|
const goal = {
|
|
id: `${now.toString(36)}${Math.random().toString(36).slice(2, 8)}`,
|
|
objective: objectiveFile ? '' : objectiveText.slice(0, GOAL_OBJECTIVE_CHAR_LIMIT),
|
|
objectiveFile,
|
|
status: 'active',
|
|
tokenBudget: tokenBudget || null,
|
|
tokensUsed: 0,
|
|
turnsUsed: 0,
|
|
blockedStreak: 0,
|
|
note: '',
|
|
statusReason: '',
|
|
lastAccountedMessageID: '',
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
};
|
|
const url = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}`);
|
|
url.searchParams.set('directory', directory);
|
|
const response = await fetch(url.toString(), {
|
|
method: 'PATCH',
|
|
headers: {
|
|
...authHeaders,
|
|
'content-type': 'application/json',
|
|
accept: 'application/json',
|
|
},
|
|
body: JSON.stringify({ metadata: { openchamber: { goal } } }),
|
|
});
|
|
if (!response.ok) throw new Error(`goal metadata patch failed (${response.status})`);
|
|
return goal;
|
|
};
|