Skip to content

feat: real generation agent transition (Phase 0-3 + 4-1/4-2/4-4/4-5)#19

Merged
smilebank7 merged 6 commits into
mainfrom
feat/agent-real-generation-remediation
Jun 11, 2026
Merged

feat: real generation agent transition (Phase 0-3 + 4-1/4-2/4-4/4-5)#19
smilebank7 merged 6 commits into
mainfrom
feat/agent-real-generation-remediation

Conversation

@smilebank7

@smilebank7 smilebank7 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

목표

데모 12개 단원에서 LLM이 실제로 문제를 생성하고, 검증 게이트가 정직하게 통과하며, 품질을 수치로 증명. (플랜: docs/specs/remediation-plan.md)

핵심 발견 (Phase 0 기준선)

DETERMINISTIC_FALLBACK=off로 측정한 순수 LLM 성공률 = 0/8 (0%). 기존 데모는 결정론 템플릿이 LLM을 가려서 통과하던 것 — docs/eval/baseline-2026-06-10.md. 이 PR는 그 0%를 움직이기 위한 기반 + 검증 정직화.

변경 요약

  • Phase 0DETERMINISTIC_FALLBACK 플래그, SSE 와이어에 gate/overall/model/refined_by 노출, scripts/eval-generation.ts 평가 하니스, 기준선 측정.
  • Phase 1 — 템플릿 last-resort 강등(+웹 "템플릿 폴백" 배지), attempt별 temperature 스케줄, generateObject 스키마실패 재시도, 게이트별 힌트+반례, intent fallback 수정(blanket must_preserve:true 제거), SOLVER_MODEL.
  • Phase 2math-engine: multiprocessing 5s 하드타임아웃, equals()+수치샘플링, /verify-answer(5종 답유형), lark LaTeX, async. agent: 게이트 3-상태(unverified 추가), 정직한 sympy/객관식 검증("비어있으면 통과" 0개), 재풀이 불일치→재시도/최종 reject, 유니코드+LaTeX 추출, domain.md I-V4 갱신, 웹 "기호 검증 불가" 배지.
  • Phase 3-1techniques_used 결정론 비교(구조 동형 기법 커버리지 강제).
  • Phase 4-2 / 4-4 — silent catch → skipped_reason 기록, LLM_E2E 게이트 통합 테스트.

검증

  • agent typecheck clean · 236 unit + 5 integration 통과
  • web typecheck clean
  • 29 pytest 통과 · ruff clean

범위 밖 (핸드오프)

시간상 단축안으로 진행 — 남은 작업(4-1 AbortController, 4-5 웹 실연동, 3-2/3-3, 4-3, 4-6, 풀 120런 eval)과 디버깅 gotcha는 docs/specs/remediation-handoff.md 에 정리.

주의

  • main 직접 변경 없음. 사용자 작업이 아닌 untracked 파일 2개(packages/agent/scripts/{transform_real_corpus.py,verify_rag.ts})와 reports/ 아티팩트는 커밋 제외.
  • 풀 라이브 eval은 미실행(1~2h) — 명령어는 baseline 문서에 박제. 수치 조작 없음.

Summary by cubic

Switches the demo to real LLM problem generation with honest, 3‑state verification and stronger math checks. Adds abortable per‑step timeouts (60s default; scaled budget for generate), wires S5/S6 to live SSE results, and runs multi‑problem requests in parallel (cap 3).

  • New Features

    • Generation and retry
      • Demote templates via DETERMINISTIC_FALLBACK (off|last-resort|first); per‑attempt temperature; schema‑failure retry with hint; counterexample feedback.
      • Optional SOLVER_MODEL builds a separate solver to decorrelate re‑solve from generation.
      • Per‑step timeout now aborts slow LLM calls and avoids premature fallback (default PER_STEP_TIMEOUT_MS 60s; generate step budget scaled; threads AbortSignal into generator/refiner/critic/solver/intent/nuance).
    • Verification
      • SymPy gate is 3‑state: passed|failed|unverified. Re‑solve mismatch triggers retry, then reject on budget.
      • Deterministic technique check via techniques_used for structural coverage; safer normalization and explicit skipped reasons.
    • Math engine
      • 5s hard timeout (multiprocessing), equivalence via exact + numeric sampling, and /verify-answer for number/expression/equation/solution_set/inequality. LaTeX parsing with lark. Async endpoints.
    • Wire/UI
      • SSE exposes gates, overall, attempt_count, generation_model, refined_by.
      • Generate endpoint fans out to min(count, 3) parallel runs; step bar streams from the first run; merges result candidates; a failed run is dropped without aborting others.
      • Web shows “템플릿 폴백” and “기호 검증 불가” badges; S5/S6 read verified results from SSE/sessionStorage; removed mocks and added result/types.ts.
    • Tooling/tests/docs
      • scripts/eval-generation.ts batch harness and LLM_E2E gated integration test; timeout‑cancellation coverage; remediation handoff updated to mark 4‑1 and 4‑5 done.
  • Dependencies

    • packages/math-engine: add lark; tune uvicorn runner (workers/reload).
    • No migrations; .env can optionally include SOLVER_MODEL.

Written for commit 112c802. Summary will update on new commits.

Review in cubic

Phase 0: DETERMINISTIC_FALLBACK env (off|last-resort|first), SSE wire exposes gates+generation_model+refined_by, scripts/eval-generation.ts harness, baseline doc (pure-LLM 0/8 verified).

Phase 1: template demoted to last-resort, per-attempt temperature schedule, generateObject schema-failure retry, gate-specific retry hints + counterexample, intent fallback no longer blanket must_preserve:true, SOLVER_MODEL env, web 템플릿 폴백 badge.

Phase 2: math-engine multiprocessing 5s hard-timeout, equals()+numeric-sampling equivalence, /verify-answer (number|expression|equation|solution_set|inequality), lark LaTeX parsing, async endpoints; agent 3-state gates (passed|failed|skipped|unverified), honest sympy/multiple-choice verification (no more non-empty=>passed), re-solve mismatch->retry/reject, equation-extractor unicode+LaTeX, domain.md I-V4 update + web 기호검증불가 badge.

Phase 3-1: techniques_used deterministic objective-gate comparison. Phase 4-2: silent catches record skipped_reason. Phase 4-4: env-gated LLM_E2E integration test.

Verified: agent typecheck clean, 236 unit + 5 integration tests pass; web typecheck clean; 29 pytest pass, ruff clean.
Covers cut tasks 4-1/4-3/4-5/4-6, 3-2/3-3, full 120-run eval, plus non-obvious gotchas (answer normalization, sameAnswer shortcut, unverified->completed wire mapping, acceptance/retry budget, py3.11 multiprocessing, lark backend).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 issues found across 64 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/agent/src/workflows/verification-workflow.ts">

<violation number="1" location="packages/agent/src/workflows/verification-workflow.ts:244">
P1: Last-resort fallback returns a different problem without re-verifying it, but reuses prior gate results. This can emit inconsistent result payloads where verification evidence does not belong to the returned problem.</violation>
</file>

<file name="packages/agent/src/schemas/wire-format.schema.ts">

<violation number="1" location="packages/agent/src/schemas/wire-format.schema.ts:84">
P2: Wire schema does not enforce the 6-gate invariant. Require `.length(6)` to keep FE payload validation aligned with verification invariants.</violation>
</file>

<file name="packages/agent/src/policies/acceptance-policy.ts">

<violation number="1" location="packages/agent/src/policies/acceptance-policy.ts:17">
P1: Configurable maxAttempts is inconsistent with invariant I-V5 fixed at 3, causing runtime invariant failures when maxAttempts > 3. Align policy limit with invariant or update invariant to use same configured cap.</violation>
</file>

<file name="packages/agent/src/schemas/source-problem.schema.ts">

<violation number="1" location="packages/agent/src/schemas/source-problem.schema.ts:62">
P2: I-S2 condition is over-restricted; `achievement_standard` is required even when `topic_code` is absent. This can reject valid normalized source rows.</violation>

<violation number="2" location="packages/agent/src/schemas/source-problem.schema.ts:65">
P1: I-S3 regex is stricter than spec and will fail valid LaTeX questions. It should only block non-normalized fraction commands.</violation>

<violation number="3" location="packages/agent/src/schemas/source-problem.schema.ts:68">
P1: I-S4 check incorrectly forbids all null explanations. This will fail legitimate dataset-30 rows that are explicitly allowed by domain invariants.</violation>

<violation number="4" location="packages/agent/src/schemas/source-problem.schema.ts:71">
P2: I-S5 condition is too broad; high-school rows can legally have null grade. Current check rejects valid source problems.</violation>
</file>

<file name="packages/agent/src/tools/latex-formatter.ts">

<violation number="1" location="packages/agent/src/tools/latex-formatter.ts:20">
P1: `\pm` expansion is algebraically incorrect because it removes the left operand. This can produce wrong candidate roots/answers for SymPy verification.</violation>

<violation number="2" location="packages/agent/src/tools/latex-formatter.ts:33">
P2: Display-math segments `\[...\]` are not extracted from prose. This leaves delimiter artifacts in normalized expressions and can break downstream parsing.</violation>

<violation number="3" location="packages/agent/src/tools/latex-formatter.ts:47">
P2: Equation extraction only matches `=` and drops inequality-only statements from prose. This can pass non-math text to SymPy normalization.</violation>
</file>

<file name="packages/agent/src/config/env.ts">

<violation number="1" location="packages/agent/src/config/env.ts:40">
P3: JSDoc for `last-resort` mode says "currently behaves like `first`" but the implementation treats it differently: generation skips template first (unlike `first`) and final-result fallback is already gated on `last-resort`. Stale docs risk misinforming configuration decisions.</violation>
</file>

<file name="docs/specs/domain.md">

<violation number="1" location="docs/specs/domain.md:171">
P3: Stale name `independent_resolve` conflicts with `re_solve` in same invariant — gate[4] step is `re_solve` per GateResult.</violation>
</file>

<file name="packages/agent/src/tools/answer-equivalence.ts">

<violation number="1" location="packages/agent/src/tools/answer-equivalence.ts:428">
P1: Choice-label regex misclassifies decimal answers as choice labels. This can pick the wrong expected choice index and produce incorrect equivalence/reject decisions.</violation>
</file>

<file name="packages/agent/src/schemas/verification.schema.ts">

<violation number="1" location="packages/agent/src/schemas/verification.schema.ts:24">
P2: New `unverified` gate status is not handled in SSE step-status mapping, causing unverified checks to be reported as completed. This mislabels verification progress/UX and hides non-decidable outcomes during the run.</violation>
</file>

<file name="scripts/eval-generation.ts">

<violation number="1" location="scripts/eval-generation.ts:474">
P2: `unverified` gate statuses are silently discarded during SSE parsing. This skews gate diagnostics and failure attribution in the eval report.</violation>
</file>

<file name="packages/agent/src/schemas/generated-problem.schema.ts">

<violation number="1" location="packages/agent/src/schemas/generated-problem.schema.ts:61">
P1: I-G2 check is applied to conceptual candidates too, causing valid conceptual transforms to be rejected when required techniques differ.</violation>

<violation number="2" location="packages/agent/src/schemas/generated-problem.schema.ts:80">
P2: I-G3 comparison is one-way subset, not equality; conceptual candidates can add extra must_preserve dimensions without failing invariant.</violation>
</file>

<file name="packages/web/hooks/use-verification-stream.ts">

<violation number="1" location="packages/web/hooks/use-verification-stream.ts:212">
P2: Gate status is not validated against the 4-state contract, so invalid payloads are treated as valid results. This can hide the “기호 검증 불가” signal instead of failing the stream payload.</violation>
</file>

<file name="packages/agent/src/steps/intent-extraction.ts">

<violation number="1" location="packages/agent/src/steps/intent-extraction.ts:72">
P3: `dimensions_source` provenance is inaccurate when strategy is null but request dimensions are provided. This weakens observability for fallback quality analysis.</violation>

<violation number="2" location="packages/agent/src/steps/intent-extraction.ts:124">
P2: Fallback intent incorrectly preserves only the first requested dimension. This can make objective mapping/verification ignore user-selected preserve constraints beyond index 0.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}

const problem = withDeterministicGeneratorMarker(fallback);
const verification = { ...input.verification, candidate_id: problem.candidate_id };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Last-resort fallback returns a different problem without re-verifying it, but reuses prior gate results. This can emit inconsistent result payloads where verification evidence does not belong to the returned problem.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/workflows/verification-workflow.ts, line 244:

<comment>Last-resort fallback returns a different problem without re-verifying it, but reuses prior gate results. This can emit inconsistent result payloads where verification evidence does not belong to the returned problem.</comment>

<file context>
@@ -183,6 +214,55 @@ export async function* runVerificationWorkflow(
+  }
+
+  const problem = withDeterministicGeneratorMarker(fallback);
+  const verification = { ...input.verification, candidate_id: problem.candidate_id };
+  assertVerificationInvariants(verification);
+  return { problem, verification };
</file context>

return {
decide(gates, attemptCount) {
if (attemptCount > 3) return "rejected";
if (attemptCount > maxAttempts) return "rejected";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Configurable maxAttempts is inconsistent with invariant I-V5 fixed at 3, causing runtime invariant failures when maxAttempts > 3. Align policy limit with invariant or update invariant to use same configured cap.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/policies/acceptance-policy.ts, line 17:

<comment>Configurable maxAttempts is inconsistent with invariant I-V5 fixed at 3, causing runtime invariant failures when maxAttempts > 3. Align policy limit with invariant or update invariant to use same configured cap.</comment>

<file context>
@@ -6,20 +6,33 @@ export interface AcceptancePolicy {
   return {
     decide(gates, attemptCount) {
-      if (attemptCount > 3) return "rejected";
+      if (attemptCount > maxAttempts) return "rejected";
 
       const byStep = new Map(gates.map((gate) => [gate.step, gate]));
</file context>

if (/\\(?:d?frac|sqrt|cdot|left|right|pi|pm)/u.test(problem.question_text)) {
throw new Error(`I-S3 violated: source problem ${problem.item_id} question_text must be normalized plain text`);
}
if (problem.explanation_text === null || problem.explanation_text.trim().length === 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: I-S4 check incorrectly forbids all null explanations. This will fail legitimate dataset-30 rows that are explicitly allowed by domain invariants.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/schemas/source-problem.schema.ts, line 68:

<comment>I-S4 check incorrectly forbids all null explanations. This will fail legitimate dataset-30 rows that are explicitly allowed by domain invariants.</comment>

<file context>
@@ -54,3 +54,21 @@ export const SourceProblemSchema = z.object({
+  if (/\\(?:d?frac|sqrt|cdot|left|right|pi|pm)/u.test(problem.question_text)) {
+    throw new Error(`I-S3 violated: source problem ${problem.item_id} question_text must be normalized plain text`);
+  }
+  if (problem.explanation_text === null || problem.explanation_text.trim().length === 0) {
+    throw new Error(`I-S4 violated: source problem ${problem.item_id} needs explanation_text`);
+  }
</file context>
Suggested change
if (problem.explanation_text === null || problem.explanation_text.trim().length === 0) {
if ((problem.explanation_text === null && problem.source_dataset !== "30") || (problem.explanation_text !== null && problem.explanation_text.trim().length === 0)) {

if (problem.achievement_standard === null || problem.achievement_standard.trim().length === 0) {
throw new Error(`I-S2 violated: source problem ${problem.item_id} needs achievement_standard`);
}
if (/\\(?:d?frac|sqrt|cdot|left|right|pi|pm)/u.test(problem.question_text)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: I-S3 regex is stricter than spec and will fail valid LaTeX questions. It should only block non-normalized fraction commands.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/schemas/source-problem.schema.ts, line 65:

<comment>I-S3 regex is stricter than spec and will fail valid LaTeX questions. It should only block non-normalized fraction commands.</comment>

<file context>
@@ -54,3 +54,21 @@ export const SourceProblemSchema = z.object({
+  if (problem.achievement_standard === null || problem.achievement_standard.trim().length === 0) {
+    throw new Error(`I-S2 violated: source problem ${problem.item_id} needs achievement_standard`);
+  }
+  if (/\\(?:d?frac|sqrt|cdot|left|right|pi|pm)/u.test(problem.question_text)) {
+    throw new Error(`I-S3 violated: source problem ${problem.item_id} question_text must be normalized plain text`);
+  }
</file context>

.replace(/\\leq?|≤/g, "<=")
.replace(/\\geq?|≥/g, ">=")
.replace(/\\pi/g, "pi")
.replace(/\\pm\s*([^\s,]+)/g, "-$1, $1")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: \pm expansion is algebraically incorrect because it removes the left operand. This can produce wrong candidate roots/answers for SymPy verification.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/tools/latex-formatter.ts, line 20:

<comment>`\pm` expansion is algebraically incorrect because it removes the left operand. This can produce wrong candidate roots/answers for SymPy verification.</comment>

<file context>
@@ -4,6 +4,50 @@ export function formatLatex(value: string): string {
+    .replace(/\\leq?|≤/g, "<=")
+    .replace(/\\geq?|≥/g, ">=")
+    .replace(/\\pi/g, "pi")
+    .replace(/\\pm\s*([^\s,]+)/g, "-$1, $1")
+    .replace(/\^/g, "**");
+  expression = extractEquationText(expression)
</file context>
Suggested change
.replace(/\\pm\s*([^\s,]+)/g, "-$1, $1")
.replace(/([^,\s]+)\s*\\pm\s*([^\s,]+)/g, "$1-$2, $1+$2")

const step = asString(o.step);
const status = asString(o.status);
if (step === null || status === null) return null;
return { step, status };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Gate status is not validated against the 4-state contract, so invalid payloads are treated as valid results. This can hide the “기호 검증 불가” signal instead of failing the stream payload.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web/hooks/use-verification-stream.ts, line 212:

<comment>Gate status is not validated against the 4-state contract, so invalid payloads are treated as valid results. This can hide the “기호 검증 불가” signal instead of failing the stream payload.</comment>

<file context>
@@ -188,6 +203,15 @@ function parsePreview(raw: unknown): string | null {
+  const step = asString(o.step);
+  const status = asString(o.status);
+  if (step === null || status === null) return null;
+  return { step, status };
+}
+
</file context>

return descriptions.map((description, index) => ({
id: String.fromCharCode(65 + index),
description,
must_preserve: index === 0,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Fallback intent incorrectly preserves only the first requested dimension. This can make objective mapping/verification ignore user-selected preserve constraints beyond index 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/steps/intent-extraction.ts, line 124:

<comment>Fallback intent incorrectly preserves only the first requested dimension. This can make objective mapping/verification ignore user-selected preserve constraints beyond index 0.</comment>

<file context>
@@ -116,3 +112,15 @@ function buildSeedIntent(
+  return descriptions.map((description, index) => ({
+    id: String.fromCharCode(65 + index),
+    description,
+    must_preserve: index === 0,
+  }));
+}
</file context>
Suggested change
must_preserve: index === 0,
must_preserve: true,


/** `first` = template short-circuits LLM when refs exist (current behavior).
* `off` = always go through LLM generator path; never substitute template.
* `last-resort` = placeholder, currently behaves like `first` (see TODO 1-1a). */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: JSDoc for last-resort mode says "currently behaves like first" but the implementation treats it differently: generation skips template first (unlike first) and final-result fallback is already gated on last-resort. Stale docs risk misinforming configuration decisions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/config/env.ts, line 40:

<comment>JSDoc for `last-resort` mode says "currently behaves like `first`" but the implementation treats it differently: generation skips template first (unlike `first`) and final-result fallback is already gated on `last-resort`. Stale docs risk misinforming configuration decisions.</comment>

<file context>
@@ -29,10 +34,19 @@ export const EnvSchema = z.object({
+
+  /** `first` = template short-circuits LLM when refs exist (current behavior).
+   *  `off` = always go through LLM generator path; never substitute template.
+   *  `last-resort` = placeholder, currently behaves like `first` (see TODO 1-1a). */
+  DETERMINISTIC_FALLBACK: z
+    .enum(["off", "last-resort", "first"])
</file context>

Comment thread docs/specs/domain.md
- (I-V2) `overall == "verified"`는 `gates[3] (sympy_verify).status == "passed"` **and** `gates[5] (objective_map).status == "passed"` 일 때만 가능 (결정론 게이트).
- (I-V3) `gates[1] (intent)`와 `gates[2] (generate)`가 passed라도 `gates[3] (sympy_verify)`가 failed면 `overall != "verified"` (D-1: LLM은 정답 판단 X).
- (I-V4) `overall == "warning"`은 SymPy는 통과했으나 `independent_resolve`(gates[4])가 불일치할 때만 사용 — 사용자에게 *주의* 라벨로 노출.
- (I-V4) `overall == "warning"`은 SymPy가 `passed` 또는 `unverified`인 상태에서 `independent_resolve`(gates[4])가 불일치했거나, 비결정 가능(non-decidable) 게이트가 `unverified`일 때 사용 — 사용자에게 *주의* 라벨로 노출. 단, 재시도 상한의 최종 attempt에서도 `re_solve` 불일치가 지속되면 `warning`이 아니라 `rejected`다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Stale name independent_resolve conflicts with re_solve in same invariant — gate[4] step is re_solve per GateResult.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/specs/domain.md, line 171:

<comment>Stale name `independent_resolve` conflicts with `re_solve` in same invariant — gate[4] step is `re_solve` per GateResult.</comment>

<file context>
@@ -168,7 +168,7 @@ type HumanFailureReason = {
 - (I-V2) `overall == "verified"`는 `gates[3] (sympy_verify).status == "passed"` **and** `gates[5] (objective_map).status == "passed"` 일 때만 가능 (결정론 게이트).
 - (I-V3) `gates[1] (intent)`와 `gates[2] (generate)`가 passed라도 `gates[3] (sympy_verify)`가 failed면 `overall != "verified"` (D-1: LLM은 정답 판단 X).
-- (I-V4) `overall == "warning"`은 SymPy는 통과했으나 `independent_resolve`(gates[4])가 불일치할 때만 사용 — 사용자에게 *주의* 라벨로 노출.
+- (I-V4) `overall == "warning"`은 SymPy가 `passed` 또는 `unverified`인 상태에서 `independent_resolve`(gates[4])가 불일치했거나, 비결정 가능(non-decidable) 게이트가 `unverified`일 때 사용 — 사용자에게 *주의* 라벨로 노출. 단, 재시도 상한의 최종 attempt에서도 `re_solve` 불일치가 지속되면 `warning`이 아니라 `rejected`다.
 - (I-V5) `attempt_count > 3`이면 강제 `overall == "rejected"` (Q-5 잠정 정책, D-9에서 확정 예정).
 
</file context>
Suggested change
- (I-V4) `overall == "warning"`은 SymPy가 `passed` 또는 `unverified`인 상태에서 `independent_resolve`(gates[4])가 불일치했거나, 비결정 가능(non-decidable) 게이트가 `unverified`일 때 사용 — 사용자에게 *주의* 라벨로 노출. 단, 재시도 상한의 최종 attempt에서도 `re_solve` 불일치가 지속되면 `warning`이 아니라 `rejected`다.
- (I-V4) `overall == "warning"`은 SymPy가 `passed` 또는 `unverified`인 상태에서 `re_solve`(gates[4])가 불일치했거나, 비결정 가능(non-decidable) 게이트가 `unverified`일 때 사용 — 사용자에게 *주의* 라벨로 노출. 단, 재시도 상한의 최종 attempt에서도 `re_solve` 불일치가 지속되면 `warning`이 아니라 `rejected`다.

} catch (err) {
const fallback = buildSeedIntent(input.request, input.strategy, input.refs);
assertIntentInvariants(fallback);
const dimensionsSource = input.strategy === null ? "guessed" : "strategy";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: dimensions_source provenance is inaccurate when strategy is null but request dimensions are provided. This weakens observability for fallback quality analysis.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/steps/intent-extraction.ts, line 72:

<comment>`dimensions_source` provenance is inaccurate when strategy is null but request dimensions are provided. This weakens observability for fallback quality analysis.</comment>

<file context>
@@ -69,6 +69,7 @@ export async function extractIntent(
   } catch (err) {
     const fallback = buildSeedIntent(input.request, input.strategy, input.refs);
     assertIntentInvariants(fallback);
+    const dimensionsSource = input.strategy === null ? "guessed" : "strategy";
     return {
       data: fallback,
</file context>
Suggested change
const dimensionsSource = input.strategy === null ? "guessed" : "strategy";
const dimensionsSource = input.strategy !== null ? "strategy" : input.request.dims.length > 0 ? "request" : "guessed";

4-1: withTimeout aborts an AbortController on timeout and threads the signal into every generateObject call (generator/refiner/critic/solver/intent/objective nuance), so slow LLM calls are cancelled instead of running to the wall clock. math-engine calls stay bounded by the client's existing internal 10s controller. Adds withTimeout cancellation tests.

4-5: drop the page-level generateMockResults seed on S5/S6; result/export views now source verified problems only from the SSE sessionStorage rehydrate. Relocate ResultProblem type to result/types.ts, delete mock.ts, add an S5 empty state. Verified S5/S6 in a real browser.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 72 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/agent/src/tools/answer-equivalence.ts">

<violation number="1" location="packages/agent/src/tools/answer-equivalence.ts:65">
P2: New answer-equivalence decision flow is duplicated across two functions, creating avoidable divergence risk. Extract shared comparison logic and pass context-specific messages/inputs.</violation>
</file>

<file name="packages/agent/src/steps/objective-mapping.ts">

<violation number="1" location="packages/agent/src/steps/objective-mapping.ts:202">
P1: Technique gate for non-conceptual mode is stricter than schema semantics; it requires all required techniques instead of at least one. This can incorrectly reject otherwise valid generations.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment on lines +202 to +206
} else if (missingRequired.length > 0) {
failures.push({
code: "technique_mismatch",
message: `Candidate techniques ${formatTechniqueList(used)} do not cover required techniques ${formatTechniqueList(missingRequired)}`,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Technique gate for non-conceptual mode is stricter than schema semantics; it requires all required techniques instead of at least one. This can incorrectly reject otherwise valid generations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/steps/objective-mapping.ts, line 202:

<comment>Technique gate for non-conceptual mode is stricter than schema semantics; it requires all required techniques instead of at least one. This can incorrectly reject otherwise valid generations.</comment>

<file context>
@@ -140,7 +169,70 @@ function evaluateDeterministically(input: ObjectiveMappingInput): Array<{ code:
+        message: `Candidate techniques ${formatTechniqueList(used)} do not overlap required or related techniques ${formatTechniqueList(related)}`,
+      });
+    }
+  } else if (missingRequired.length > 0) {
+    failures.push({
+      code: "technique_mismatch",
</file context>
Suggested change
} else if (missingRequired.length > 0) {
failures.push({
code: "technique_mismatch",
message: `Candidate techniques ${formatTechniqueList(used)} do not cover required techniques ${formatTechniqueList(missingRequired)}`,
});
} else if (required.length > 0 && missingRequired.length === required.length) {
failures.push({
code: "technique_mismatch",
message: `Candidate techniques ${formatTechniqueList(used)} do not overlap required techniques ${formatTechniqueList(required)}`,
});

return { status: "undecidable", reason: withSkippedReasons("math-engine could not compare the answers", debug) };
}

export async function decideAnswerMatchesSolutions(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: New answer-equivalence decision flow is duplicated across two functions, creating avoidable divergence risk. Extract shared comparison logic and pass context-specific messages/inputs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/tools/answer-equivalence.ts, line 65:

<comment>New answer-equivalence decision flow is duplicated across two functions, creating avoidable divergence risk. Extract shared comparison logic and pass context-specific messages/inputs.</comment>

<file context>
@@ -3,10 +3,123 @@ import type { MathEngineClient } from "./math-engine-client.js";
+  return { status: "undecidable", reason: withSkippedReasons("math-engine could not compare the answers", debug) };
+}
+
+export async function decideAnswerMatchesSolutions(
+  mathEngine: MathEngineClient,
+  answer: string,
</file context>

@smilebank7 smilebank7 changed the title feat: real generation agent transition (Phase 0-3 + 4-2/4-4) feat: real generation agent transition (Phase 0-3 + 4-1/4-2/4-4/4-5) Jun 10, 2026
@smilebank7

Copy link
Copy Markdown
Contributor Author

추가: Phase 4-1 + 4-5 (commit 0a000ae0f)

핸드오프의 高가치 두 항목을 이어서 구현·검증 완료.

4-1 — step 타임아웃 시 LLM 호출 실제 취소

  • withTimeout(fn, opts)AbortController를 만들고 타임아웃 시 abort → fn(signal)로 전달.
  • signal을 6개 generateObject 호출 전부에 전파 (generator / refiner / critic / solver / intent / objective nuance) via agent 인터페이스(하위호환 optional param).
  • 기준선 160s 타임아웃 근본원인 해소 — 느린 LLM 호출이 벽시계까지 안 가고 취소됨.
  • math-engine 호출은 client 내부 per-request 10s AbortController로 이미 bounded → 의도적으로 over-plumbing 안 함 (rationale는 핸드오프 §3-9에 기록).
  • withTimeout 취소 단위 테스트 +2 (타임아웃 시 signal abort 발화 / 정상 완료 시 미발화).

4-5 — S5/S6 실 결과 연동, mock 제거

  • result/page.tsx·export/page.tsx의 페이지 레벨 generateMockResults seed 제거 → view는 SSE→sessionStorage rehydrate에서만 검증 문항을 가져옴.
  • ResultProblem 타입 result/types.ts로 이전, mock.ts 삭제, S5 빈 상태 추가.

검증

  • agent typecheck clean · 238 unit + 5 integration green · web typecheck + build green · 29 pytest green (pre-push).
  • 실 브라우저 E2E (playwright, production source via next dev):
    • /result (저장값 없음) → "아직 검증된 문항이 없어요" 빈 상태. mock 4문항 누수 없음.
    • /result (sessionStorage 주입) → 2문항 rehydrate, KaTeX 렌더 + 템플릿폴백·기호검증불가 badge 정상.
    • /export?adopted=... → 채택 1문항만 시험지에 필터링.
    • console error 0.

남은 작업(3-2/3-3, 4-3, 4-6, 풀 eval)은 docs/specs/remediation-handoff.md에 갱신.

The 30s per-step timeout killed the generate step in last-resort/off mode: real generation runs generator + 2 critic + 2 refiner LLM calls (~90s observed), and a single intent call alone takes ~36s on a reasoning model. With the 4-1 abort wired in, that timeout cancelled the in-flight LLM call and surfaced a generic orchestrator error, which is why the demo silently fell back to the deterministic template. Raise the default per-step timeout to 60s and give the generate step a budget scaled to its max LLM-call count (perStep x (1 + 2*maxCriticRounds)). Verified: last-resort now yields a real gpt-5.5-generated, SymPy-verified problem.
The workflow produces one problem per run; the demo expects multiple (3문항). Fan out at the route/SSE layer: run min(count,3) workflows concurrently, drive the user-visible step bar from the first run only, drain the rest silently, and merge their result candidates into one result event. Orchestrator and its 238 tests are untouched. A failed parallel run contributes no problem instead of aborting the others. Verified live: count=5 -> 3 distinct gpt-5.5 verified problems, one 12-event step bar, 169s.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/agent/src/server/routes/generate.ts">

<violation number="1" location="packages/agent/src/server/routes/generate.ts:23">
P2: Route hard-caps `request.count` to 3, violating the API contract and silently returning fewer problems than requested.</violation>
</file>

<file name="packages/agent/src/server/sse/progress-stream.ts">

<violation number="1" location="packages/agent/src/server/sse/progress-stream.ts:73">
P1: Forwarding driver error events in parallel mode causes the frontend to abort before merged results arrive. A single failed driver run can hide successful candidates from other runs.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

collected.push(event);
continue;
}
if (event.type === "error") forwardedError = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Forwarding driver error events in parallel mode causes the frontend to abort before merged results arrive. A single failed driver run can hide successful candidates from other runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/server/sse/progress-stream.ts, line 73:

<comment>Forwarding driver error events in parallel mode causes the frontend to abort before merged results arrive. A single failed driver run can hide successful candidates from other runs.</comment>

<file context>
@@ -32,3 +32,66 @@ export async function pipeProgressToSse(
+        collected.push(event);
+        continue;
+      }
+      if (event.type === "error") forwardedError = true;
+      await writeWire(event);
+    }
</file context>

zValidator("json", GenerateRequestSchema),
(c) => {
const request = c.req.valid("json");
const count = Math.min(Math.max(request.count, 1), MAX_PARALLEL_GENERATIONS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Route hard-caps request.count to 3, violating the API contract and silently returning fewer problems than requested.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/src/server/routes/generate.ts, line 23:

<comment>Route hard-caps `request.count` to 3, violating the API contract and silently returning fewer problems than requested.</comment>

<file context>
@@ -18,8 +20,12 @@ export function createGenerateRoute(deps: VerificationWorkflowDeps, options?: Ru
     zValidator("json", GenerateRequestSchema),
     (c) => {
       const request = c.req.valid("json");
+      const count = Math.min(Math.max(request.count, 1), MAX_PARALLEL_GENERATIONS);
       return streamSSE(c, async (stream) => {
-        await pipeProgressToSse(stream, runVerificationWorkflow(deps, request, options));
</file context>

@smilebank7 smilebank7 merged commit 12ee3ed into main Jun 11, 2026
5 checks passed
@smilebank7 smilebank7 deleted the feat/agent-real-generation-remediation branch June 11, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant