feat: distill any oversized goal objective, not just plans

The distillation moved from PlanView into setSessionGoal — the shared
choke point for every goal source (composer, fork dialog, plans) — and
into the scheduled-tasks goal creation server-side. Any objective over
5000 chars is distilled into completion criteria by the small model; on a
transient failure a head+tail excerpt keeps the intent (top) and the
acceptance criteria (bottom) with a trim marker, plus a toast. The
working agent always received the full prompt in chat — only the auditor
is bound by the limit.
This commit is contained in:
Bohdan Triapitsyn
2026-07-12 16:34:16 +03:00
parent 19b2a3d0d3
commit 8d956f5f9c
14 changed files with 99 additions and 51 deletions
+12 -25
View File
@@ -30,9 +30,6 @@ import { useProjectsStore } from '@/stores/useProjectsStore';
import { useSelectionStore } from '@/sync/selection-store';
import { useConfigStore } from '@/stores/useConfigStore';
import { useSessionGoalArmStore } from '@/stores/useSessionGoalArmStore';
import { SESSION_GOAL_OBJECTIVE_CHAR_LIMIT } from '@/lib/sessionGoalMetadata';
import { distillPlanForGoal } from '@/lib/smallModel';
import { toast } from '@/components/ui';
import { useUIStore } from '@/stores/useUIStore';
import { useGitStore } from '@/stores/useGitStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
@@ -608,27 +605,17 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
// its file); on distillation failure a head+tail excerpt keeps the
// intent (top) and acceptance criteria (bottom), sacrificing the
// implementation middle the agent reads from the file anyway.
let goalObjective: string | null = null;
if (execution.runAsGoal === true) {
const header = [
`Implement the plan "${sendPromptTitle}" end-to-end${resolvedPath ? ` (plan file: ${resolvedPath})` : ''}.`,
'Re-read that file for full details — it is the source of truth.',
].join(' ');
const budget = SESSION_GOAL_OBJECTIVE_CHAR_LIMIT - header.length - 2;
if (content.length <= budget) {
goalObjective = `${header}\n\n${content}`;
} else {
const distilled = await distillPlanForGoal(content);
if (distilled) {
goalObjective = `${header}\n\n${distilled.slice(0, budget)}`;
} else {
const marker = '\n\n[… plan trimmed for the auditor — the plan file has the full version …]\n\n';
const half = Math.max(0, Math.floor((budget - marker.length) / 2));
goalObjective = `${header}\n\n${content.slice(0, half)}${marker}${content.slice(-half)}`;
toast.error(t('plans.goal.toast.distillFallback'));
}
}
}
// Oversized objectives (huge plans) are distilled into audit
// criteria inside setSessionGoal — the shared path for every goal
// source. Here we only compose header + full content.
const goalObjective = execution.runAsGoal === true
? [
`Implement the plan "${sendPromptTitle}" end-to-end${resolvedPath ? ` (plan file: ${resolvedPath})` : ''}.`,
'Re-read that file for full details — it is the source of truth.',
'',
content,
].join('\n')
: null;
useSessionGoalArmStore.getState().setArmed(execution.runAsGoal === true, goalObjective);
await sendMessage(
visiblePrompt,
@@ -646,7 +633,7 @@ export const PlanView: React.FC<PlanViewProps> = ({ targetPath = null }) => {
setIsPlanSendSubmitting(false);
}
},
[canCreateWorktree, content, createSession, currentProjectRef, initializeNewOpenChamberSession, pendingPlanSend, resolvedPath, routeToChat, sendMessage, sendPromptTitle, setCurrentSession, t]
[canCreateWorktree, content, createSession, currentProjectRef, initializeNewOpenChamberSession, pendingPlanSend, resolvedPath, routeToChat, sendMessage, sendPromptTitle, setCurrentSession]
);
const blockWidgets = React.useMemo(() => {