You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a771f86
Browse filesBrowse the repository at this point in the historyBrowse files
Write R2 cases in parallel parts, and give each argument field a word target
R2 cases came within 6 s of the 130 s request budget (124 s for a Hungarian
case at medium effort). Both Supabase projects are on the Free plan, which
stops a worker at 150 s, so the budget cannot grow. An R2 case is now planned
at low effort (our two arguments and the claims to answer), then written in
four parts at the same time at the student's model and effort. Part 1 writes
the speech and streams to the browser. Each part has its own smaller cap, which
keeps its reasoning budget at 8,000 tokens at medium. Measured: 68 s, 58 s and
53 s, down from 124, 109 and 105. A failed part stops the others and every
call is billed.
R1 speeches ran to 752 and 823 words against a 600-word budget. The Speech
Budget line now gives each argument field a word target derived from the
speaker's time and the number of arguments. Measured: 533 and 531.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: .claude/skills/verify-debatelab/features/case-generation.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# Case generation
2
2
3
-
A signed-in user enters a motion on Generate and receives a structured debate case: an opening (hook and "we will show N arguments" line), arguments (title, point, explanation, evidence, why it matters), an "If they say…" list, a closing line and a sources list. The role shapes it: the Introductor may add one definition line; R2 brings no new arguments, only `Backing up our case` (the team's earlier arguments) and "If they say…" with one full answer for each thing the other side is likely to say; the Conclusor has 2-3 "Why we win" points instead of new arguments. Saved cases from before still show their old sections under plain names (`How to judge this debate`, `What we have to show`, `They may also say`) when they have content.
3
+
A signed-in user enters a motion on Generate and receives a structured debate case: an opening (hook and "we will show N arguments" line), arguments (title, point, explanation, evidence, why it matters), an "If they say…" list, a closing line and a sources list. The role shapes it: the Introductor may add one definition line; R2 brings no new arguments, only `Backing up our case` (the team's earlier arguments) and "If they say…" with one full answer for each thing the other side is likely to say; the Conclusor has 2-3 "Why we win" points instead of new arguments. An R2 case makes five provider calls, all `generate` in `providerCalls()`: a plan, then four parts written at the same time; the last one in the list still carries `Role: r2`. Saved cases from before still show their old sections under plain names (`How to judge this debate`, `What we have to show`, `They may also say`) when they have content.
Copy file name to clipboardExpand all lines: docs/architecture.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,7 +82,7 @@ Every structured answer is shown while the model writes it. A page passes a `liv
82
82
83
83
A streamed run keeps its worker alive with `EdgeRuntime.waitUntil` ([`_shared/background.ts`](../supabase/functions/_shared/background.ts)) from the first event until the HTTP layer has read the last one. Supabase otherwise counts the response as sent once its headers are out and may retire the worker mid-run (`EarlyDrop`). Streamed Ask (`ai-stream`) is held the same way until the browser has read its final event. A run still unanswered about 142 s after its request arrived is aborted and gets a retryable timeout `result` instead of a connection cut at the 150 s wall clock; when the aborted run answers within 2 s, that answer is sent instead, so it says `charged: false` when the units went back. Storing the result and waiting for the browser to read it are cut short to finish before the wall clock. The request sends its id as `resultKey`. The function then writes a row to `ai_run_results` when the run starts, and stores the `result` there before sending it ([`ai/run-results.ts`](../supabase/functions/ai/run-results.ts); one row per user and feature, readable only by its owner). When the stream drops, stalls (no keepalive for 45 s), or delivers a result it cannot read, [`src/lib/ai/run-recovery.ts`](../src/lib/ai/run-recovery.ts) polls that row until the run's result appears, then resolves or fails exactly as if the event had arrived. A row still without a status long after it started is a run its worker never finished. A browser Stop, reload, or closed tab still cancels the run. The server cannot tell those from a lost connection, so it stops spending as soon as the connection goes.
84
84
85
-
A reply that hits its token cap is written again once with twice the cap, after a `reset`. The Generate cap (12,000 tokens) leaves room for a full comprehensive case on Claude Haiku, so a long case finishes on its first attempt. Claude rejects an assistant prefill, so a cut-off reply cannot be continued where it stopped. Sparring calls send a strict `json_schema` (`sparringOutputSchema` in `ai/sparring-persistence.ts`). Anthropic's structured outputs take no numeric or array-size limits, so the 0-25 range, the four categories, and the 12-item lists are enforced by the parser. When a reply parses but has the wrong shape, the log records only the failing field path, never the model's text. The call is then retried once with a repair prompt that names the field. A second failure answers `invalid_output` (502, logged as `invalid_output`), which the browser shows as a malformed answer. The judge also retries once when Claude is briefly unavailable or its stream breaks. The round has already paid, so retries cost the student nothing.
85
+
A reply that hits its token cap is written again once with twice the cap, after a `reset`. The Generate cap (12,000 tokens) leaves room for a full comprehensive case on Claude Haiku, so a long case finishes on its first attempt. Claude rejects an assistant prefill, so a cut-off reply cannot be continued where it stopped. An R2 case is the longest reply (the speech and 10-12 full answers), and one call wrote it in up to 124 s of the 130 s budget, which cannot grow because both Supabase projects are on the Free plan and stop a worker at 150 s. So [`ai/r2-case.ts`](../supabase/functions/ai/r2-case.ts) asks for a short plan first (our two arguments and the claims to answer, at low effort), then writes four parts at the same time at the student's model and effort: part 1 the speech and its share of the answers, streamed to the browser, the others only answers. Each part has its own smaller cap (`caseWriterPart`), which also keeps its reasoning budget small enough to finish well inside the budget. One failed part fails the case, stops the others, and bills every call. Sparring calls send a strict `json_schema` (`sparringOutputSchema` in `ai/sparring-persistence.ts`). Anthropic's structured outputs take no numeric or array-size limits, so the 0-25 range, the four categories, and the 12-item lists are enforced by the parser. When a reply parses but has the wrong shape, the log records only the failing field path, never the model's text. The call is then retried once with a repair prompt that names the field. A second failure answers `invalid_output` (502, logged as `invalid_output`), which the browser shows as a malformed answer. The judge also retries once when Claude is briefly unavailable or its stream breaks. The round has already paid, so retries cost the student nothing.
86
86
87
87
For a validated Crossfire next turn, the server returns the reply first with a short-lived signed sidecar token. The client then requests a Jev classification of only the newest completed question-answer exchange. The token is atomically claimed through its own one-use counter, separate from the product's daily AI quota. The optional directness, concession, and follow-up signal is attached by turn ID to the answer; a missing, slow, replayed, forged, or failed sidecar never delays or invalidates the main reply. Draft version 1 remains compatible because turn signals are optional.
return`With ${count} arguments, each argument is about ${perArgument} spoken words: claim one sentence of at most ${claim} words, reasoning about ${reasoning} words, evidence about ${evidence} words, whyItMatters about ${whyItMatters} words. opening.hook and caseStructure.thesis at most 35 words each, closingLine one sentence of at most 25 words. Aim for these numbers, not under them: a shorter field leaves the speaker's time unused, a longer one runs past it.`;
173
+
}
174
+
154
175
/** The word budget the speaker reads out, stated in the request so speeches fit their time. */
consthead=`${minutes} minutes, about ${words} spoken words at ${SPOKEN_WORDS_PER_MINUTE} words a minute.`;
160
181
switch(role){
161
182
case"intro":
162
-
return`${head} The opening, the "we will show" line, the arguments in short form and the closing line together stay within ${words} words. The "If they say..." answers are a separate short prep list.`;
183
+
return`${head} The opening, the "we will show" line, the arguments in short form and the closing line together stay within ${words} words. ${argumentWordTargets(words,length)}The "If they say..." answers are a separate short prep list.`;
163
184
case"r1":
164
-
return`${head} The opening, the arguments and the closing line together stay within about ${words-180} words, leaving time for the "If they say..." answers the speaker picks.`;
185
+
return`${head} The opening, the arguments and the closing line together stay within about ${words-180} words, leaving time for the "If they say..." answers the speaker picks.${argumentWordTargets(words-180,length)}`;
165
186
case"r2":
166
187
return`${head} The opening, the backing-up paragraphs and the closing line together stay within about 300 words (about 2 minutes); the rest of the time is for the "If they say..." blocks the other side actually used.`;
167
188
default:
@@ -202,7 +223,7 @@ export function buildGenerateUserPrompt(input: Record<string, unknown>, searchCo
202
223
conversational: "Use natural spoken-round phrasing without slang or filler.",
203
224
aggressive: "Use sharper strategic phrasing and clearer attack language while staying professional.",
204
225
}[tone]||"Use polished, judge-facing language with controlled phrasing.";
return`=== PLANNING STEP: do not write the case yet ===
264
+
This R2 case is written in parts at the same time, all from the plan you write now. This step overrides the Length Instruction and the output format: return ONLY this JSON object, in the output language:
265
+
{"ourArguments": [{"title": "a few words", "claim": "one sentence"}, {"title": "a few words", "claim": "one sentence"}], "theirClaims": ["one sentence", "..."]}
266
+
- ourArguments: exactly 2. The team's earlier arguments when CURRENT DEBATE CONTEXT has them, by their own names; otherwise the two strongest arguments for our side that the research backs.
267
+
- theirClaims: exactly ${counterCount} different things the other side is likely to say, each one sentence the way they would put it: first their 2-3 main arguments, then the supporting points they will use to back each of them up, then the answers they will give to each of our two arguments. No two claims make the same point. Prefer claims the attached research lets us answer with facts.`;
268
+
}
269
+
270
+
/**
271
+
* The instruction for one part. Part 1 writes the speech (opening, backing up
272
+
* our case, closing) and its share of the answers; every other part writes
273
+
* only answers. Answers are numbered across the whole case.
constanswers=`exactly ${claims.length} counters, one for each of these claims, in this order, each using the claim as its targetArgument:\n${list}\nEach answer about ${r2AnswerWords(length)} words, built as the R2 rules ask, and tied to our arguments by name.`;
279
+
consthead=`=== WRITING PART ${part} OF ${parts}: claims ${firstNumber}-${firstNumber+claims.length-1} ===
280
+
This R2 case is written in ${parts} parts at the same time, from one plan. This part overrides the Length Instruction's counts.
281
+
Our arguments, in this order (call them "our first argument" and "our second argument"):
282
+
${ours}`;
283
+
returnpart===1
284
+
? `${head}
285
+
Write the opening, caseStructure, exactly ${plan.ourArguments.length} reinforce items (one for each of our arguments, in order), the closingLine, and ${answers}
286
+
contentions stays an empty array. sources lists every source this part names.`
287
+
: `${head}
288
+
Write only "If they say..." answers: return a JSON object with only "counters" and "sources". Write ${answers}
289
+
No opening, reinforce, whyWeWin or closingLine: another part writes them. sources lists every source this part names.`;
0 commit comments