Answer budget within the context window; enforced reviews of completed todos

Context window
- Requests asked for the model's full max_output, but providers count
  input + output against the window: long chats with large-output models
  (DeepSeek V4.1 Flash, 384k) failed with "input too long" before automatic
  compaction (85 %) kicked in. max_tokens is now what the window leaves
  (estimated input = last usage + messages since), the chat is compacted when
  less than 32k would remain for an answer, and a context-overflow error
  compacts and retries once.
- ModelConfig.max_input (catalogue limit.input, editable in the provider
  settings) lowers the compaction threshold for models with an input limit.

Enforced reviews (modes with refine.rounds > 0, refine.after_todos default on)
- Every todo marked completed is reviewed against the work since the last
  review; with findings it is reopened and the feedback goes to the agent
  (max. rounds per todo). Passed reviews leave a visible note.
- Ending the turn with open todos sends the agent back to work while it makes
  progress (max. 10 reminders in a row without progress); such modes may take
  up to 2,000 steps per turn. Reaching the step limit is reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-10 23:12:49 +01:00
1 parent ca14ba3338
commit bd896794fd
14 files changed
+386 -27

No files matched your search

+34 -5
View File
@@ -115,7 +115,36 @@ function BackgroundResult({ text }: { text: string }) {
);
}
const REFINE_RE = /^<refine-feedback round="([^"]*)" reviewer="([^"]*)">\n?([\s\S]*?)\n?<\/refine-feedback>/;
const REFINE_RE = /^<refine-feedback round="([^"]*)" reviewer="([^"]*)"(?: todos="\d+")?>\n?([\s\S]*?)\n?<\/refine-feedback>/;
/** Enforced reviews: a completed todo passed, or a reminder about open todos. */
const NOTE_RE = /^<(refine-approved|todo-reminder)(?: reviewer="([^"]*)")?(?: open="\d+")?>\n?([\s\S]*?)\n?<\/(?:refine-approved|todo-reminder)>/;
/** Messages from the harness shown as cards instead of user bubbles. */
const isHarnessNote = (text: string) => BG_RE.test(text) || REFINE_RE.test(text) || NOTE_RE.test(text);
/** A todo passed its review, or the agent was sent back to its open todos. */
function HarnessNote({ text }: { text: string }) {
const t = useT();
const m = text.match(NOTE_RE);
if (!m) return null;
const [, kind, reviewer, body] = m;
const items = body.split("\n").map((l) => l.replace(/^- /, "").trim()).filter(Boolean);
const approved = kind === "refine-approved";
return (
<div className={`harness-note ${approved ? "ok" : ""}`}>
<Icon name={approved ? "check" : "refresh"} />
<span className="grow">
<b>{t(approved ? "chat.reviewPassed" : "chat.todoReminder", { n: items.length })}</b> {items.join(" · ")}
</span>
{reviewer && <span className="chip">{reviewer}</span>}
</div>
);
}
function HarnessMessage({ text }: { text: string }) {
if (REFINE_RE.test(text)) return <RefineFeedback text={text} />;
if (NOTE_RE.test(text)) return <HarnessNote text={text} />;
return <BackgroundResult text={text} />;
}
/** Findings of the review loop, sent back to the agent. */
function RefineFeedback({ text }: { text: string }) {
@@ -469,14 +498,14 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string
);
}
const allTexts = m.parts.filter((p) => p.type === "text") as { text: string }[];
const background = allTexts.filter((p) => BG_RE.test(p.text) || REFINE_RE.test(p.text));
const texts = allTexts.filter((p) => !BG_RE.test(p.text) && !REFINE_RE.test(p.text));
const background = allTexts.filter((p) => isHarnessNote(p.text));
const texts = allTexts.filter((p) => !isHarnessNote(p.text));
const imgs = m.parts.filter((p) => p.type === "image") as { image: ImageData }[];
if (texts.length === 0 && imgs.length === 0) {
return background.length ? (
<div key={m.id} className="msg">
{background.map((p, i) => (
REFINE_RE.test(p.text) ? <RefineFeedback key={i} text={p.text} /> : <BackgroundResult key={i} text={p.text} />
<HarnessMessage key={i} text={p.text} />
))}
</div>
) : null;
@@ -484,7 +513,7 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string
return (
<div key={m.id} className="msg">
{background.map((p, i) => (
REFINE_RE.test(p.text) ? <RefineFeedback key={i} text={p.text} /> : <BackgroundResult key={i} text={p.text} />
<HarnessMessage key={i} text={p.text} />
))}
<div className="msg-user">
{!busy && !readOnly && (
+6
View File
@@ -376,6 +376,7 @@ interface ModelRow {
name?: string;
context?: number;
max_output?: number;
max_input?: number;
reasoning?: boolean;
vision?: boolean;
effort?: string;
@@ -454,6 +455,7 @@ function ProviderEditor({ name, p, reload, status }: { name: string; p: Json; re
price_multipliers: f.price_multipliers ?? m.price_multipliers,
context: m.context ?? f.context,
max_output: m.max_output ?? f.max_output,
max_input: m.max_input ?? f.max_input ?? undefined,
vision: m.vision || !!f.vision,
reasoning: m.reasoning || !!f.reasoning,
};
@@ -616,6 +618,7 @@ function ProviderEditor({ name, p, reload, status }: { name: string; p: Json; re
<th>ID</th>
<th>{t("settings.context")}</th>
<th>{t("settings.maxOutput")}</th>
<th title={t("settings.maxInputHint")}>{t("settings.maxInput")}</th>
<th title={t("settings.reasoning")}><Icon name="brain" /></th>
<th title={t("settings.vision")}><Icon name="eye" /></th>
<th>{t("settings.effort")}</th>
@@ -640,6 +643,9 @@ function ProviderEditor({ name, p, reload, status }: { name: string; p: Json; re
<td>
<input type="number" value={m.max_output ?? ""} onChange={(e) => setModel(i, { max_output: num(e.target.value) })} />
</td>
<td>
<input type="number" value={m.max_input ?? ""} placeholder="–" onChange={(e) => setModel(i, { max_input: num(e.target.value) })} />
</td>
<td>
<input type="checkbox" checked={!!m.reasoning} onChange={(e) => setModel(i, { reasoning: e.target.checked })} />
</td>
+7 -1
View File
@@ -61,6 +61,7 @@ function AgentRow({ a, raw, tools, onSaved }: { a: AgentInfo; raw: any; tools: T
refine_rounds: String(raw?.refine?.rounds ?? a.refine?.rounds ?? 0),
refine_model: raw?.refine?.model ?? "",
refine_criteria: raw?.refine?.criteria ?? "",
refine_after_todos: (raw?.refine?.after_todos ?? a.refine?.after_todos ?? true) as boolean,
});
const builtin = a.source === "builtin" || (a.source === "config" && !raw?.prompt && ["build", "plan", "craft", "chat", "general", "explore"].includes(a.name));
@@ -74,7 +75,7 @@ function AgentRow({ a, raw, tools, onSaved }: { a: AgentInfo; raw: any; tools: T
const rounds = Math.max(0, Math.min(10, Number(form.refine_rounds) || 0));
patches.push({
path: [...base, "refine"],
value: rounds || form.refine_model || form.refine_criteria ? { rounds, model: form.refine_model || null, criteria: form.refine_criteria || null } : null,
value: rounds || form.refine_model || form.refine_criteria ? { rounds, model: form.refine_model || null, criteria: form.refine_criteria || null, after_todos: form.refine_after_todos } : null,
});
}
if (!builtin || form.description) patches.push({ path: [...base, "description"], value: form.description || null });
@@ -158,6 +159,11 @@ function AgentRow({ a, raw, tools, onSaved }: { a: AgentInfo; raw: any; tools: T
<label>{t("agents.refineCriteria")}</label>
<textarea rows={3} value={form.refine_criteria} onChange={(e) => setForm({ ...form, refine_criteria: e.target.value })} />
<div className="hint">{t("agents.refineCriteriaHint")}</div>
<label className="check">
<input type="checkbox" checked={form.refine_after_todos} onChange={(e) => setForm({ ...form, refine_after_todos: e.target.checked })} />
{t("agents.refineAfterTodos")}
</label>
<div className="hint">{t("agents.refineAfterTodosHint")}</div>
</>
)}
<label>{t("agents.description")}</label>
+6
View File
@@ -334,6 +334,10 @@ export const de: Messages = {
"wizard.idea.cadBox": "Eine Box mit Schnappdeckel, Innenmaß 80 × 50 × 30 mm",
"wizard.idea.cadBracket": "Eine Wandhalterung für ein 25-mm-Rohr mit zwei M4-Schraublöchern",
"activity.reviewing": "Ergebnis wird geprüft (Runde {round})…",
"chat.reviewPassed": "Review bestanden:",
"chat.todoReminder": "Weiterarbeiten – {n} offene Todos:",
"agents.refineAfterTodos": "Jedes erledigte Todo prüfen und bis zum Ende der Liste arbeiten",
"agents.refineAfterTodosHint": "Jedes Todo, das der Agent als erledigt markiert, wird geprüft; bei Befunden wird es wieder geöffnet. Beendet er den Turn mit offenen Todos, arbeitet er weiter (für unbeaufsichtigte Läufe, z. B. über Nacht). Aus: nur ein Review am Ende des Turns.",
"chat.reviewRound": "Review {round}: Änderungen nötig",
"agents.refine": "Review-Schleife",
"agents.refineRounds": "Review-Runden pro Aufgabe",
@@ -757,6 +761,8 @@ export const de: Messages = {
"settings.pricesHint": "Preise pro 1 Mio. Tokens in der eingestellten Währung. Cache-Preise fallen auf den Input-Preis zurück.",
"settings.modelsEmptyHint": "Keine Modelle eingetragen: Sie werden automatisch beim Provider abgefragt.",
"settings.context": "Kontext",
"settings.maxInput": "Max. Eingabe",
"settings.maxInputHint": "Eingabe-Limit, falls der Anbieter eines unterhalb des Kontextfensters setzt (leer: das Kontextfenster). Der Chat wird zusammengefasst, bevor die Eingabe es erreicht.",
"settings.maxOutput": "Max. Ausgabe",
"settings.reasoning": "Reasoning",
"settings.vision": "Bilder",
+6
View File
@@ -333,6 +333,10 @@ export const en = {
"wizard.idea.cadBox": "A box with a snap-on lid, inner size 80 × 50 × 30 mm",
"wizard.idea.cadBracket": "A wall bracket for a 25 mm pipe with two M4 screw holes",
"activity.reviewing": "Reviewing the result (round {round})…",
"chat.reviewPassed": "Review passed:",
"chat.todoReminder": "Back to work – {n} open todos:",
"agents.refineAfterTodos": "Review every completed todo and work until the list is done",
"agents.refineAfterTodosHint": "Each todo the agent marks completed is reviewed; with findings it is reopened. Ending the turn with open todos sends the agent back to work (for unattended runs, e.g. overnight). Off: only one review at the end of the turn.",
"chat.reviewRound": "Review {round}: changes requested",
"agents.refine": "Review loop",
"agents.refineRounds": "Review rounds per task",
@@ -756,6 +760,8 @@ export const en = {
"settings.pricesHint": "Prices per 1M tokens in the configured currency. Cache prices default to the input price.",
"settings.modelsEmptyHint": "No models listed: they are discovered automatically from the provider.",
"settings.context": "Context",
"settings.maxInput": "Max input",
"settings.maxInputHint": "Input limit, if the provider sets one below the context window (empty: the context window). The chat is compacted before the input reaches it.",
"settings.maxOutput": "Max output",
"settings.reasoning": "Reasoning",
"settings.vision": "Vision",
+29
View File
@@ -1068,6 +1068,13 @@ button.session-group:hover {
.form label {
color: var(--fg2);
}
.form label.check {
grid-column: 2;
display: flex;
align-items: center;
gap: 8px;
color: var(--fg1);
}
.form .hint {
grid-column: 2;
font-size: 12px;
@@ -3164,6 +3171,28 @@ button.danger-solid {
.refine-card summary .icon {
color: var(--purple);
}
.harness-note {
display: flex;
align-items: baseline;
gap: 8px;
margin: 6px 0;
padding: 6px 10px;
border-left: 3px solid var(--blue);
border-radius: 4px;
background: var(--bg1);
font-size: 12.5px;
color: var(--fg2);
}
.harness-note .icon {
color: var(--blue);
flex-shrink: 0;
}
.harness-note.ok {
border-left-color: var(--green);
}
.harness-note.ok .icon {
color: var(--green);
}
/* --- automations ---------------------------------------------------------------- */
.job.off {
+2 -1
View File
@@ -141,6 +141,7 @@ export interface ModelInfo {
name: string;
context?: number;
max_output?: number | null;
max_input?: number | null;
reasoning: boolean;
vision: boolean;
effort?: string | null;
@@ -166,7 +167,7 @@ export interface AgentInfo {
tools?: string[] | null;
source: "builtin" | "config" | "file" | "personal";
max_parallel?: number | null;
refine?: { rounds: number; model?: string | null; criteria?: string | null } | null;
refine?: { rounds: number; model?: string | null; criteria?: string | null; after_todos?: boolean } | null;
/** Source file of Markdown agents. */
path?: string | null;
}
+10
View File
@@ -3,6 +3,16 @@
* under "unreleased"; scripts/release.sh renames the key to the release version.
*/
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
unreleased: {
en: [
"Modes with a review: every completed todo is reviewed, and the agent keeps working until the todo list is done – for overnight runs",
"Long chats are compacted in time instead of failing with “input too long”",
],
de: [
"Modi mit Review: jedes erledigte Todo wird geprüft, und der Agent arbeitet weiter, bis die Liste abgehakt ist – für Läufe über Nacht",
"Lange Chats werden rechtzeitig zusammengefasst, statt mit „Eingabe zu lang“ abzubrechen",
],
},
"202611-00017": {
en: ["Your messages are shown as Markdown; the eye button next to the text field previews it before sending"],
de: ["Deine Nachrichten werden als Markdown dargestellt; das Augen-Symbol neben dem Textfeld zeigt vor dem Senden eine Vorschau"],