Skip to content

Wire CLIProxy-backed source-grounded generation#9

Merged
smilebank7 merged 20 commits into
mainfrom
feat/product-docs
Jun 2, 2026
Merged

Wire CLIProxy-backed source-grounded generation#9
smilebank7 merged 20 commits into
mainfrom
feat/product-docs

Conversation

@smilebank7

@smilebank7 smilebank7 commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Wire the agent runtime through prompt/strategy/RAG loaders, SSE wire format, and CLIProxy-compatible LLM generation.
  • Pass selected source problems from the web flow into /api/generate, preserving generated results through result/export.
  • Add seed RAG strategies/corpus and backend contract tests for the demo generation path.

Verification

  • pnpm -F @openmath/agent typecheck
  • pnpm -F @openmath/web typecheck
  • pnpm -F @openmath/agent test
  • pnpm -F @openmath/agent build
  • pnpm -F @openmath/web build
  • pnpm test (pre-push hook: Node + Python)
  • Manual Codex OAuth CLIProxy QA for structural and conceptual flows

Notes

  • No local .env or OAuth credential files are included.
  • .env.example contains placeholders only.
  • Local artifacts such as .playwright-mcp/, AI_HUB_data/, and extra untracked product previews were left uncommitted.

Summary by cubic

Wired source-grounded generation end to end with a CLIProxy/OpenAI‑compatible LLM and SSE streaming; shipped the full S0–S6 product flow using the canonical JSONL RAG client. Implemented 6-step verification with specialist agents, per-step policies, and exposed rejected results with a visual guard (I‑G4).

  • New Features

    • /api/generate streams SSE in the FE wire format with a new wire adapter; CORS enabled for http://localhost:3001.
    • LLM provider resolver for cliproxy, openai, and openai-compatible; prompt/strategy FS loaders; LaTeX formatter; math-engine HTTP client.
    • Full pipeline: RAG search, intent extraction, problem generation (source-grounded via selected source and dims), SymPy verification, independent re-solve, and objective mapping with nuance; acceptance/retry/timeout policies.
    • Canonical in-memory RAG client with seed corpus and source-problem matching; problem-generator prompt updated (lower temp, explicit source context).
    • FE updates: S0–S6 carry source through verify/result/export; SSE hook accepts sourceProblemText (@microsoft/fetch-event-source); result filters drop “실패” (policy shows failed as cards); KaTeX rendering; nav/landing refresh; favicon.
    • Docs: README LLM setup, and architecture/domain updates clarifying I‑G4.
  • Migration

    • In @openmath/agent, copy .env.example. Set LLM_PROVIDER and model:
      • cliproxy: LLM_PROVIDER=cliproxy, LLM_BASE_URL=http://localhost:8317/v1, LLM_API_KEY=..., LLM_MODEL=gpt-4o (or Claude/Gemini).
      • openai: LLM_PROVIDER=openai, OPENAI_API_KEY, OPENAI_MODEL.
      • openai-compatible: LLM_PROVIDER=openai-compatible, LLM_BASE_URL, LLM_API_KEY, LLM_MODEL.
    • Without an LLM, generation/verification fails (seed RAG only echoes sources and is rejected by objective_map guard).
    • Run agent on 3000 and web on 3001; FE connects to /api/generate via SSE (CORS preconfigured).

Written for commit c307268. Summary will update on new commits.

Review in cubic

ldm310 and others added 9 commits May 20, 2026 04:34
랜딩 추가 컴포넌트 + 출제 워크플로우 전 화면 구현.

랜딩:
- components/landing/footer.tsx: 3-col (소개·도움말·피드백) + fine-print
- components/landing/faq.tsx: 4 native disclosure-row
- components/landing/book-stage.tsx: iPad showcase 5-step walkthrough
  (S0→S1→S2→S3→S4 자동 순환, BrandMark 사용)
- components/landing/nav.tsx: 3-grid 정중앙 + 로그인 버튼
- hero CTA: /app, /samples 로 연결

출제 워크플로우 (S0~S6, productivity surface):
- /app: S0 워크스페이스 (hero-tile + 2 entry cards)
- /app/new/grade: S1 학년 선택 (3 intent-radio-card)
- /app/new/topic: S2 단원 선택 (filter-chip + 30 단원 정적 데이터)
- /app/new/intent: S3 동형 모드 + 평가 차원 (radio + checkbox)
- /app/new/verify: S4 검증 6 단계 (SSE 실시간)
- /app/new/result: S5 결과 + 채택 (mock.ts, agent 미연동)
- /app/new/export: S6 PDF 출력 (브라우저 print API)

부가 페이지:
- /login: 로그인 폼 (mock auth — 제출 시 /app 라우팅)
- /samples: 검증 통과 예시 3 문항

공통 인프라:
- hooks/use-verification-stream.ts: @microsoft/fetch-event-source 기반
  SSE 컨슈머 + AbortController + React 18 strict-mode 가드
- components/math/latex-renderer.tsx: KaTeX SSR (block + inline +
  $..$ / $$..$$ 자동 분할 LatexMixed)
- components/app/primary-nav.tsx: productivity nav

DESIGN.md 준수:
- btn-primary drop shadow 제거 (brand-mark glow 재현 금지 규칙)
- 한 fold 1 primary: nav 버튼 → btn-ghost / btn-secondary
- LaTeX 항상 formula-stage (soft-cloud) 위 — S5 풀이 inline 전환
- inline hex 0건, as any/@ts-ignore 0건
- prefers-reduced-motion: spinner-dot, intent cards, step rows 등 커버

기타:
- .gitignore: docs/product/ 화이트리스트 (USER_FLOW, SCREENS, MOCKUPS)
- README: 작업 진행 상태 + 라우트 맵 + 신규 deps + mock 한계 명시
- globals.css: orphan CSS ~1000줄 정리 (옛 book v1/v2, tablet v1,
  mock-intro/result, token-v2, tablet-signal/brand-mark/login-btn)
- 새 deps: @microsoft/fetch-event-source 2.0.1, katex 0.16.x
P1: stabilize SSE hook deps via ref snapshot; render verify preview LaTeX with KaTeX renderer instead of raw text.

P2: reset stream state on reconnect (drop stale steps/preview/error); neutralize print-dialog notice (afterprint cannot tell save vs cancel); disable v2-only result actions with sr-only describedby; drop role="img"/aria-label on formula stages so KaTeX MathML reaches screen readers.

P3: SCREENS typo brower-native -> browser-native; simplify export defaultTitle via useState lazy init; drop stepIdx from book-stage interval deps (functional updater is sufficient).
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

@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.

11 issues found across 68 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/web/app/app/new/verify/page.tsx">

<violation number="1" location="packages/web/app/app/new/verify/page.tsx:20">
P3: This page duplicates query parsing logic (`parseMode`/`parseDims`) that already exists in nearby pages; extract and reuse a shared parser to prevent divergence.</violation>
</file>

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

<violation number="1" location="packages/agent/src/index.ts:45">
P2: Hard-coded `allowedHosts` blocks all non-local LLM base URLs, breaking valid `openai-compatible` / `anthropic-via-compatible` configurations.</violation>
</file>

<file name="packages/agent/src/steps/sympy-verification.ts">

<violation number="1" location="packages/agent/src/steps/sympy-verification.ts:59">
P1: Hard-failing when `question_text` lacks `=` makes all non-equation candidates automatically rejected by policy.</violation>

<violation number="2" location="packages/agent/src/steps/sympy-verification.ts:80">
P2: Use a word-bounded, case-insensitive `or` delimiter to avoid malformed answer tokenization.</violation>
</file>

<file name="packages/agent/src/tools/schema-loader.ts">

<violation number="1" location="packages/agent/src/tools/schema-loader.ts:46">
P2: `load(code)` does not verify that the YAML's `strategy.code` matches the requested code, which can return/cache the wrong strategy for that lookup.</violation>
</file>

<file name="packages/agent/src/tools/llm-provider.ts">

<violation number="1" location="packages/agent/src/tools/llm-provider.ts:50">
P2: This adds duplicate base-URL validation logic that already exists in `math-engine-client.ts`; centralize it in a shared utility to avoid security-rule drift.</violation>
</file>

<file name="packages/agent/src/schemas/generate-request.schema.ts">

<violation number="1" location="packages/agent/src/schemas/generate-request.schema.ts:43">
P1: The new topic requirement treats empty strings as valid, so invalid requests can pass schema validation and fail later in retrieval.</violation>

<violation number="2" location="packages/agent/src/schemas/generate-request.schema.ts:55">
P2: `getGenerateRequestTopicCode` can return an empty `topic_code` and ignore a valid `topic`, which breaks fallback semantics.</violation>
</file>

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

Re-trigger cubic

Comment thread packages/agent/src/server/sse/wire-adapter.ts

source_problem_text: z.string().optional(),
}).superRefine((request, ctx) => {
if (request.topic === undefined && request.topic_code === undefined) {

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: The new topic requirement treats empty strings as valid, so invalid requests can pass schema validation and fail later in retrieval.

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

<comment>The new topic requirement treats empty strings as valid, so invalid requests can pass schema validation and fail later in retrieval.</comment>

<file context>
@@ -25,14 +25,32 @@ export const GenerateRequestSchema = z.object({
 
   source_problem_text: z.string().optional(),
+}).superRefine((request, ctx) => {
+  if (request.topic === undefined && request.topic_code === undefined) {
+    ctx.addIssue({
+      code: z.ZodIssueCode.custom,
</file context>
Suggested change
if (request.topic === undefined && request.topic_code === undefined) {
if ((request.topic?.trim() ?? "") === "" && (request.topic_code?.trim() ?? "") === "") {

mathEngine: MathEngineClient,
candidate: GeneratedProblem,
): Promise<boolean> {
if (!candidate.question_text.includes("=")) {

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: Hard-failing when question_text lacks = makes all non-equation candidates automatically rejected by policy.

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

<comment>Hard-failing when `question_text` lacks `=` makes all non-equation candidates automatically rejected by policy.</comment>

<file context>
@@ -16,8 +16,88 @@ export interface SympyVerificationOutput {
+  mathEngine: MathEngineClient,
+  candidate: GeneratedProblem,
+): Promise<boolean> {
+  if (!candidate.question_text.includes("=")) {
+    throw new Error("SymPy verification supports equation candidates only");
+  }
</file context>

Comment thread packages/web/README.md
}
const strategy = await readStrategyFile(join(opts.strategiesDir, `${code}.yaml`));
if (strategy === null) return null;
cache.set(strategy.code, 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.

P2: load(code) does not verify that the YAML's strategy.code matches the requested code, which can return/cache the wrong strategy for that lookup.

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

<comment>`load(code)` does not verify that the YAML's `strategy.code` matches the requested code, which can return/cache the wrong strategy for that lookup.</comment>

<file context>
@@ -22,7 +31,64 @@ export interface FsStrategyLoaderOptions {
+    }
+    const strategy = await readStrategyFile(join(opts.strategiesDir, `${code}.yaml`));
+    if (strategy === null) return null;
+    cache.set(strategy.code, strategy);
+    return strategy;
+  }
</file context>
Suggested change
cache.set(strategy.code, strategy);
if (strategy.code !== code) {
throw new Error(`Strategy code mismatch in ${code}.yaml: expected ${code}, got ${strategy.code}`);
}
cache.set(code, strategy);

Comment thread packages/agent/src/tools/rag-client.ts Outdated
export type GenerateRequest = z.infer<typeof GenerateRequestSchema>;

export function getGenerateRequestTopicCode(request: GenerateRequest): string {
return request.topic_code ?? request.topic ?? "";

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: getGenerateRequestTopicCode can return an empty topic_code and ignore a valid topic, which breaks fallback semantics.

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

<comment>`getGenerateRequestTopicCode` can return an empty `topic_code` and ignore a valid `topic`, which breaks fallback semantics.</comment>

<file context>
@@ -25,14 +25,32 @@ export const GenerateRequestSchema = z.object({
 export type GenerateRequest = z.infer<typeof GenerateRequestSchema>;
+
+export function getGenerateRequestTopicCode(request: GenerateRequest): string {
+  return request.topic_code ?? request.topic ?? "";
+}
</file context>
Suggested change
return request.topic_code ?? request.topic ?? "";
return request.topic_code?.trim() || request.topic?.trim() || "";

Comment thread packages/agent/src/index.ts Outdated
modelId: llmModel,
baseUrl: llmBaseUrl,
apiKey: llmApiKey ?? "openmath-local",
allowedHosts: ["localhost", "127.0.0.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.

P2: Hard-coded allowedHosts blocks all non-local LLM base URLs, breaking valid openai-compatible / anthropic-via-compatible configurations.

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

<comment>Hard-coded `allowedHosts` blocks all non-local LLM base URLs, breaking valid `openai-compatible` / `anthropic-via-compatible` configurations.</comment>

<file context>
@@ -1,14 +1,70 @@
+          modelId: llmModel,
+          baseUrl: llmBaseUrl,
+          apiKey: llmApiKey ?? "openmath-local",
+          allowedHosts: ["localhost", "127.0.0.1"],
+        }),
+        modelId: llmModel,
</file context>
Suggested change
allowedHosts: ["localhost", "127.0.0.1"],
allowedHosts: env.LLM_PROVIDER === "cliproxy" ? ["localhost", "127.0.0.1"] : undefined,


function parseExpectedSolutions(answer: string): string[] {
return answer
.split(/[,;]|또는|or/)

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: Use a word-bounded, case-insensitive or delimiter to avoid malformed answer tokenization.

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

<comment>Use a word-bounded, case-insensitive `or` delimiter to avoid malformed answer tokenization.</comment>

<file context>
@@ -16,8 +16,88 @@ export interface SympyVerificationOutput {
+
+function parseExpectedSolutions(answer: string): string[] {
+  return answer
+    .split(/[,;]|또는|or/)
+    .map((part) => part.trim())
+    .map((part) => part.replace(/^[a-zA-Z]\s*=\s*/, ""))
</file context>
Suggested change
.split(/[,;]||or/)
.split(/[,;]||\bor\b/i)

};
};

function parseMode(raw: string | null): "structural" | "conceptual" | null {

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: This page duplicates query parsing logic (parseMode/parseDims) that already exists in nearby pages; extract and reuse a shared parser to prevent divergence.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web/app/app/new/verify/page.tsx, line 20:

<comment>This page duplicates query parsing logic (`parseMode`/`parseDims`) that already exists in nearby pages; extract and reuse a shared parser to prevent divergence.</comment>

<file context>
@@ -0,0 +1,49 @@
+  };
+};
+
+function parseMode(raw: string | null): "structural" | "conceptual" | null {
+  if (raw === "structural" || raw === "conceptual") return raw;
+  return null;
</file context>

smilebank7 added 11 commits May 28, 2026 00:11
Resolve 15 conflicts after PR #7 (web S0-S6, squashed) and PR #8 (RAG
canonical JSONL client) landed on main.

- packages/agent/src/tools/rag-client.ts: take main's canonical
  OpenMathRagRecord-based client (D-7) and integrate source_problem_text
  matching from this branch (source-grounded generation).
- 14 web files: take ours (this branch). PR #7 was squash-merged so its
  files appeared as add/add; this branch already contains PR #7's
  contents plus subsequent commits (source-grounded workflow, demo
  navigation polish, BrandMark removal in favor of plain wordmark).
  Verified: no commits touched packages/web/ on main after PR #7 merge.
@smilebank7 smilebank7 enabled auto-merge (squash) June 2, 2026 05:59

@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.

11 issues found across 30 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/web/app/globals.css">

<violation number="1" location="packages/web/app/globals.css:2213">
P2: Keyboard-focus branch is unreachable because `.book-cover` elements are not focusable, so `:focus-visible` never applies.</violation>
</file>

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

<violation number="1" location="packages/agent/src/tools/latex-formatter.ts:29">
P1: Global `*` replacement corrupts unresolved power operators (`**`) into whitespace, producing mathematically incorrect LaTeX output.</violation>
</file>

<file name="packages/agent/src/agents/refiner-agent.ts">

<violation number="1" location="packages/agent/src/agents/refiner-agent.ts:39">
P2: RefinerAgent ignores its configured refiner prompt/model and always reuses GeneratorAgent, making refiner-specific wiring dead and potentially bypassing intended refinement behavior.</violation>
</file>

<file name="packages/agent/src/steps/problem-generation.ts">

<violation number="1" location="packages/agent/src/steps/problem-generation.ts:109">
P1: The failure path never returns `gate: failed` because `await Promise.reject(err)` throws during object construction.</violation>

<violation number="2" location="packages/agent/src/steps/problem-generation.ts:137">
P2: Calling `mathEngine.solve` on raw `question_text` can hard-fail generation for non-parseable text that includes `=`.</violation>
</file>

<file name="packages/web/components/landing/book-stage.tsx">

<violation number="1" location="packages/web/components/landing/book-stage.tsx:134">
P2: Parallax mouse tracking is bound to `window`, so off-stage mouse movement keeps driving stage transforms and breaks the intended stage-local behavior.</violation>
</file>

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

<violation number="1" location="packages/agent/src/steps/objective-mapping.ts:161">
P3: Duplicate `normalizeMathText` logic was added instead of reusing a shared utility, creating a drift-prone normalization path.</violation>
</file>

<file name="packages/agent/.env.example">

<violation number="1" location="packages/agent/.env.example:28">
P3: Canonical alias priority is described backwards. The comment says to use `LLM_BASE_URL`/`LLM_API_KEY`/`LLM_MODEL` "when none of the above 3 options map" (fallback), but in `src/index.ts` they are the **highest-priority** values checked first via `??`. If a user follows 옵션 2 (OpenAI direct) by uncommenting `OPENAI_API_KEY`/`OPENAI_MODEL` and switching `LLM_PROVIDER=openai` without also disabling the canonical aliases, `LLM_BASE_URL=http://localhost:8317/v1` and `LLM_API_KEY=dummy-key` would silently override the OpenAI settings — routing to CLIProxy with dummy key instead of OpenAI.</violation>
</file>

<file name="packages/agent/src/steps/independent-resolve.ts">

<violation number="1" location="packages/agent/src/steps/independent-resolve.ts:92">
P2: `parseAnswers` splits on `or` as a plain substring, which can corrupt valid expressions (e.g. `floor(x)`) and create false re-solve mismatches.</violation>
</file>

<file name="packages/agent/prompts/problem-generator.md">

<violation number="1" location="packages/agent/prompts/problem-generator.md:60">
P0: Prompt instructs LLM to output `question_text` and `expected_answer` in KaTeX LaTeX format, but the downstream SymPy verification pipeline expects standard SymPy expressions. The math engine (`packages/math-engine/src/routers/math.py`) uses `sympy.parsing.sympy_parser.parse_expr` with `standard_transformations + implicit_multiplication_application + convert_xor` — this can parse `x**2`, `sqrt(7)`, `2*x + 3` but CANNOT parse LaTeX syntax like `x^{2}`, `\sqrt{7}`, `5x`. No LaTeX-to-SymPy conversion exists in the pipeline. The `formatLatex` function (`packages/agent/src/tools/latex-formatter.ts`) converts from SymPy → KaTeX, not the reverse. Result: when the LLM follows the prompt and outputs LaTeX, both the generation step's `normalizeExpectedAnswer` (in `problem-generation.ts:137`) and the verification step's `verifyWithSympy` (in `sympy-verification.ts:68,73`) will send unparseable expressions to the math engine, causing HTTP 400 parse errors and cascading step failures.</violation>
</file>

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

<violation number="1" location="packages/agent/src/workflows/verification-workflow.ts:66">
P2: Top-level dependency assertion can throw before first yield, causing `/api/generate` SSE to terminate without a structured error event.</violation>
</file>

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

Re-trigger cubic

- `proposed_solution_trace`에 풀이 단계 명시
- `inferred_intent.objective_code == intent.objective_code` (I-G1)
JSON으로만 응답.
- `question_text`는 KaTeX가 바로 렌더링할 수 있는 LaTeX `x` 방정식 문자열이어야 한다. `**`, `*`를 쓰지 말고 지수는 `x^{2}`, 곱셈은 `5x`, 제곱근은 `\\sqrt{7}`처럼 쓴다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0: Prompt instructs LLM to output question_text and expected_answer in KaTeX LaTeX format, but the downstream SymPy verification pipeline expects standard SymPy expressions. The math engine (packages/math-engine/src/routers/math.py) uses sympy.parsing.sympy_parser.parse_expr with standard_transformations + implicit_multiplication_application + convert_xor — this can parse x**2, sqrt(7), 2*x + 3 but CANNOT parse LaTeX syntax like x^{2}, \sqrt{7}, 5x. No LaTeX-to-SymPy conversion exists in the pipeline. The formatLatex function (packages/agent/src/tools/latex-formatter.ts) converts from SymPy → KaTeX, not the reverse. Result: when the LLM follows the prompt and outputs LaTeX, both the generation step's normalizeExpectedAnswer (in problem-generation.ts:137) and the verification step's verifyWithSympy (in sympy-verification.ts:68,73) will send unparseable expressions to the math engine, causing HTTP 400 parse errors and cascading step failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/agent/prompts/problem-generator.md, line 60:

<comment>Prompt instructs LLM to output `question_text` and `expected_answer` in KaTeX LaTeX format, but the downstream SymPy verification pipeline expects standard SymPy expressions. The math engine (`packages/math-engine/src/routers/math.py`) uses `sympy.parsing.sympy_parser.parse_expr` with `standard_transformations + implicit_multiplication_application + convert_xor` — this can parse `x**2`, `sqrt(7)`, `2*x + 3` but CANNOT parse LaTeX syntax like `x^{2}`, `\sqrt{7}`, `5x`. No LaTeX-to-SymPy conversion exists in the pipeline. The `formatLatex` function (`packages/agent/src/tools/latex-formatter.ts`) converts from SymPy → KaTeX, not the reverse. Result: when the LLM follows the prompt and outputs LaTeX, both the generation step's `normalizeExpectedAnswer` (in `problem-generation.ts:137`) and the verification step's `verifyWithSympy` (in `sympy-verification.ts:68,73`) will send unparseable expressions to the math engine, causing HTTP 400 parse errors and cascading step failures.</comment>

<file context>
@@ -57,8 +57,10 @@ updated: 2026-05-18
 JSON으로만 응답.
-- `question_text`는 SymPy가 풀 수 있는 `x` 방정식 문자열이어야 한다. 예: `x**2 - 7 = 0`, `2*x + 3 = 11`.
-- `expected_answer`는 `sqrt(7), -sqrt(7)`처럼 쉼표로 구분한다. 서버가 SymPy로 다시 계산해 최종 정답을 검증한다.
+- `question_text`는 KaTeX가 바로 렌더링할 수 있는 LaTeX `x` 방정식 문자열이어야 한다. `**`, `*`를 쓰지 말고 지수는 `x^{2}`, 곱셈은 `5x`, 제곱근은 `\\sqrt{7}`처럼 쓴다.
+- `expected_answer`도 LaTeX로 쓴다. 예: `2, 5`, `\\sqrt{7}, -\\sqrt{7}`.
+- 예시 출력: `{ "question_text": "x^{2} - 7x + 10 = 0", "expected_answer": "2, 5", "proposed_solution_trace": "인수분해하면 (x - 2)(x - 5) = 0이므로 x = 2 또는 x = 5이다." }`
</file context>

}

function formatMultiplication(value: string): string {
return value.replace(/\s*\*\s*/g, " ");

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: Global * replacement corrupts unresolved power operators (**) into whitespace, producing mathematically incorrect LaTeX output.

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 29:

<comment>Global `*` replacement corrupts unresolved power operators (`**`) into whitespace, producing mathematically incorrect LaTeX output.</comment>

<file context>
@@ -0,0 +1,87 @@
+}
+
+function formatMultiplication(value: string): string {
+  return value.replace(/\s*\*\s*/g, " ");
+}
+
</file context>

};
} catch (err) {
return {
data: await Promise.reject(err),

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: The failure path never returns gate: failed because await Promise.reject(err) throws during object construction.

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

<comment>The failure path never returns `gate: failed` because `await Promise.reject(err)` throws during object construction.</comment>

<file context>
@@ -27,16 +33,111 @@ export interface ProblemGenerationInput {
+    };
+  } catch (err) {
+    return {
+      data: await Promise.reject(err),
+      gate: {
+        step: "generate",
</file context>

display: flex;
gap: 8px;
.book-cover:hover .book-cover-front,
.book-cover:focus-visible .book-cover-front {

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: Keyboard-focus branch is unreachable because .book-cover elements are not focusable, so :focus-visible never applies.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web/app/globals.css, line 2213:

<comment>Keyboard-focus branch is unreachable because `.book-cover` elements are not focusable, so `:focus-visible` never applies.</comment>

<file context>
@@ -2059,722 +2059,426 @@
-    display: flex;
-    gap: 8px;
+  .book-cover:hover .book-cover-front,
+  .book-cover:focus-visible .book-cover-front {
+    transform: rotateY(-168deg);
   }
</file context>

...input.hints.map((hint) => `- ${hint}`),
].join("\n");

return deps.generator.generate({

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: RefinerAgent ignores its configured refiner prompt/model and always reuses GeneratorAgent, making refiner-specific wiring dead and potentially bypassing intended refinement behavior.

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

<comment>RefinerAgent ignores its configured refiner prompt/model and always reuses GeneratorAgent, making refiner-specific wiring dead and potentially bypassing intended refinement behavior.</comment>

<file context>
@@ -16,9 +21,29 @@ export interface RefinerAgent {
+        ...input.hints.map((hint) => `- ${hint}`),
+      ].join("\n");
+
+      return deps.generator.generate({
+        request: input.request,
+        intent: input.intent,
</file context>

Comment on lines +134 to +138
window.addEventListener("mousemove", handleMove, { passive: true });
node.addEventListener("mouseleave", handleLeave);
return () => {
window.removeEventListener("mousemove", handleMove);
node.removeEventListener("mouseleave", handleLeave);

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: Parallax mouse tracking is bound to window, so off-stage mouse movement keeps driving stage transforms and breaks the intended stage-local behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/web/components/landing/book-stage.tsx, line 134:

<comment>Parallax mouse tracking is bound to `window`, so off-stage mouse movement keeps driving stage transforms and breaks the intended stage-local behavior.</comment>

<file context>
@@ -69,287 +112,98 @@ export function BookStage() {
+      node.style.setProperty("--mouse-x", "0");
+      node.style.setProperty("--mouse-y", "0");
+    };
+    window.addEventListener("mousemove", handleMove, { passive: true });
+    node.addEventListener("mouseleave", handleLeave);
+    return () => {
</file context>
Suggested change
window.addEventListener("mousemove", handleMove, { passive: true });
node.addEventListener("mouseleave", handleLeave);
return () => {
window.removeEventListener("mousemove", handleMove);
node.removeEventListener("mouseleave", handleLeave);
node.addEventListener("mousemove", handleMove, { passive: true });
node.addEventListener("mouseleave", handleLeave);
return () => {
node.removeEventListener("mousemove", handleMove);
node.removeEventListener("mouseleave", handleLeave);
};


function parseAnswers(answer: string): string[] {
return answer
.split(/[,;]|또는|or/)

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: parseAnswers splits on or as a plain substring, which can corrupt valid expressions (e.g. floor(x)) and create false re-solve mismatches.

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

<comment>`parseAnswers` splits on `or` as a plain substring, which can corrupt valid expressions (e.g. `floor(x)`) and create false re-solve mismatches.</comment>

<file context>
@@ -16,12 +18,89 @@ export interface IndependentResolveInput {
+
+function parseAnswers(answer: string): string[] {
+  return answer
+    .split(/[,;]|또는|or/)
+    .map((part) => part.trim())
+    .map((part) => part.replace(/^[a-zA-Z]\s*=\s*/, ""))
</file context>
Suggested change
.split(/[,;]||or/)
.split(/[,;]||\bor\b/i)

options?: RunOptions,
): AsyncGenerator<WorkflowYield, WorkflowReturn, void> {
throw new Error("runVerificationWorkflow: not implemented yet");
assertRequiredDeps(deps);

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: Top-level dependency assertion can throw before first yield, causing /api/generate SSE to terminate without a structured error event.

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 66:

<comment>Top-level dependency assertion can throw before first yield, causing `/api/generate` SSE to terminate without a structured error event.</comment>

<file context>
@@ -63,15 +61,20 @@ export type WorkflowReturn = {
-  _options?: RunOptions,
+  options?: RunOptions,
 ): AsyncGenerator<WorkflowYield, WorkflowReturn, void> {
+  assertRequiredDeps(deps);
+
   const timestamp = () => new Date().toISOString();
</file context>
Suggested change
assertRequiredDeps(deps);
try {
assertRequiredDeps(deps);
} catch (err) {
yield {
type: "error",
stage: "orchestrator",
code: "workflow_dependencies_missing",
message: err instanceof Error ? err.message : String(err),
recoverable: false,
timestamp: new Date().toISOString(),
};
return { verifications: [] };
}

return normalizeMathText(left) === normalizeMathText(right);
}

function normalizeMathText(value: string): string {

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: Duplicate normalizeMathText logic was added instead of reusing a shared utility, creating a drift-prone normalization path.

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 161:

<comment>Duplicate `normalizeMathText` logic was added instead of reusing a shared utility, creating a drift-prone normalization path.</comment>

<file context>
@@ -2,31 +2,165 @@
+  return normalizeMathText(left) === normalizeMathText(right);
+}
+
+function normalizeMathText(value: string): string {
+  return value
+    .replace(/²/g, "**2")
</file context>

# OPENAI_API_KEY=sk-...
# OPENAI_MODEL=gpt-4o

# Canonical aliases — 위 3가지 옵션 중 어느 것도 매핑되지 않으면 이 3개로 직접 지정 가능

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: Canonical alias priority is described backwards. The comment says to use LLM_BASE_URL/LLM_API_KEY/LLM_MODEL "when none of the above 3 options map" (fallback), but in src/index.ts they are the highest-priority values checked first via ??. If a user follows 옵션 2 (OpenAI direct) by uncommenting OPENAI_API_KEY/OPENAI_MODEL and switching LLM_PROVIDER=openai without also disabling the canonical aliases, LLM_BASE_URL=http://localhost:8317/v1 and LLM_API_KEY=dummy-key would silently override the OpenAI settings — routing to CLIProxy with dummy key instead of OpenAI.

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

<comment>Canonical alias priority is described backwards. The comment says to use `LLM_BASE_URL`/`LLM_API_KEY`/`LLM_MODEL` "when none of the above 3 options map" (fallback), but in `src/index.ts` they are the **highest-priority** values checked first via `??`. If a user follows 옵션 2 (OpenAI direct) by uncommenting `OPENAI_API_KEY`/`OPENAI_MODEL` and switching `LLM_PROVIDER=openai` without also disabling the canonical aliases, `LLM_BASE_URL=http://localhost:8317/v1` and `LLM_API_KEY=dummy-key` would silently override the OpenAI settings — routing to CLIProxy with dummy key instead of OpenAI.</comment>

<file context>
@@ -1,20 +1,31 @@
+# OPENAI_API_KEY=sk-...
+# OPENAI_MODEL=gpt-4o
+
+# Canonical aliases — 위 3가지 옵션 중 어느 것도 매핑되지 않으면 이 3개로 직접 지정 가능
 LLM_BASE_URL=http://localhost:8317/v1
 LLM_API_KEY=dummy-key
</file context>
Suggested change
# Canonical aliases — 위 3가지 옵션 중 어느 것도 매핑되지 않으면 이 3개로 직접 지정 가능
# Canonical aliases — CLIProxy/OpenAI 를 통틀어 최우선 적용. 아래 LLM_BASE_URL/LLM_API_KEY/LLM_MODEL 이 설정되면 CLIPROXY_*, OPENAI_* 보다 먼저 사용됨. (옵션 2 로 전환 시 다른 값과 충돌하지 않도록 비활성화)

@smilebank7 smilebank7 merged commit b099d91 into main Jun 2, 2026
5 checks passed
@smilebank7 smilebank7 deleted the feat/product-docs branch June 2, 2026 06:06
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