Commit Graph
20 Commits
Author SHA1 Message Date
d7b31aa5e7 fix(generation): add browser-safe package entry (#1609)
Co-authored-by: SY <sy@SYdeMacBook-Pro.local>
Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-09-20 16:10:52 +08:00
wyuc 3b4567076f refactor(generation): consume the package everywhere and delete lib/generation (#1090)
Part D of #1057 — the switch. All consumers import @openmaic/generation; lib/generation deleted (net -11,900 lines). PBL wiring preserved (single-call -> classified fallback -> injected loop), streaming outline route deduplicated onto buildOutlinePrompt, serverExternalPackages wired, and the change verified by a real-model four-kind end-to-end run against the production build with a rendered-classroom screenshot check. Closes #1057.
2026-08-10 03:58:33 -04:00
wyuc 12814079ba refactor(pbl): consume the contract's PBL project types (#1079)
Third and final PR for #1061. lib/pbl/v2/types.ts becomes a barrel over @openmaic/dsl plus a recursive app-side runtime overlay (plain Omit & Overlay after the dsl contract types were made precise again — index signatures removed, schema tolerance moved into generation post-processing). Write validator byte-identical to base; negative type tests and keyof sentinels pin the contract surface. dsl 0.8.0, storage 0.2.3 (comment-only). Closes #1061.
2026-08-09 23:15:03 -04:00
wyuc 0ae21aa559 refactor(pbl): retire v1 write path and dead UI, converge on v2 (#1060)
* refactor(pbl): retire v1 write path and dead UI (#1058)

Remove the legacy generator, renderer branches, chat API, and compatibility mirror writes.

Keep stored v1 scenes readable through documented legacy adapters, and surface ordinary PBL planner double failures instead of degrading.

* fix(pbl): contain planner failure to the scene, support legacy scenes in action regeneration, tighten legacy guards

- Contain typed planner failures per scene while preserving retry and skip behavior.
- Upgrade legacy PBL content before regenerating scene actions.
- Apply video-export import boundaries to legacy readers.
- Harden legacy rendering and empty-state handling.
- Restore legacy upgrade compatibility coverage.
- Remove retired translations and refresh fallback narration and legacy cover documentation.

* fix(pbl): keep tolerant legacy configs renderable, guard invalid chat timestamps

* chore(dsl): bump to 0.6.3 for README accuracy fix

The README no longer references the deleted PBLProjectConfig symbol; the version gate requires a bump for any publishable package input change.

* fix(pbl): harden legacy upgrade and persistence contract

* fix(pbl): align action prompt with v2 workflow and validate project containers

* fix(i18n): remove retired PBL v1 keys from the new fr-FR locale

The fr-FR locale landed on main after this branch removed the v1 UI key families; prune the same 38 keys so all 10 locales align with en-US.

* fix(generation): sync packaged pbl-actions template with the v2 wording

The generation package that landed on main carries a byte-identical copy of the app's pbl-actions template; sync the v2 rewording and bump the package version.

* fix(pbl): normalize legacy content and preserve planner status

* fix(pbl): propagate provider status and ground action prompts

* fix(pbl): classify planner failures for retry, default the project summary prompt input

* fix(pbl): fall back to legacy data when v2 payloads are damaged, harden summary building

* refactor(pbl): resolve hybrid content through a validity-aware resolver

* fix(pbl): keep one v2 acceptance criterion across validator, resolver and persistence

Persistence gates on the same container predicate as the validator and the renderer's resolver: payloads the renderer will not treat as v2 are inert bytes everywhere, avoiding runtime side effects for documents the write barrier would reject. An explicit null proficiencyAssessment stays processable.

* fix(pbl): require runnable projects and harden legacy fallback

* test(pbl): exercise the real save reload and hydration chain

* fix(pbl): require an instructor and usable microtasks before v2 takes precedence

* fix(pbl): fall back to the loop planner unless the provider failed or the user cancelled

* fix(pbl): require role and task identities before v2 takes precedence

Runtime normalization can repair status and create the instructor thread, but it cannot synthesize the role id that binds the thread, the microtask ids used by lookup and update paths, or the labels the workspace renders. The runnable predicate and its video-export mirror now require them, so identity-less payloads fall back to usable legacy data.
2026-08-08 23:26:17 -04:00
wyuc 3ca86aab95 test(pbl-v2): pin single-call prompt goldens and the loop planner's injection seam (#1071)
Follow-up to #1069: golden coverage for all four planner prompt variants (the single-call pair is production's first-priority path), an assertion pinning the loop's injected call contract (toolset, stopWhen, source label, thinking config), plus three review-identified cleanups. No production behavior changes.
2026-08-06 05:49:29 -04:00
wyuc 3f5c51bdba refactor(pbl-v2): split the planner core from the loop; inject the LLM call path (#1069)
Implements #1062. Pure planner-core extracted from the loop's file; both planners receive their LLM entry as a parameter (call sites pass the real callLLM unchanged); operations/ split into kernel vs runtime with a lint-enforced boundary; planner prompts pinned byte-identical via fixtures generated from the pre-refactor implementation.
2026-08-06 05:28:56 -04:00
wyucandClaude Opus 5 97f8585728 refactor(pbl-v2): route runtime agents through the shared LLM entry point (#1006)
* refactor(pbl-v2): route runtime agents through the shared LLM entry point

The five PBL v2 runtime call sites (instructor x2, evaluator, simulator x2)
invoked the AI SDK directly while the other 21 call sites in lib/ and app/ go
through callLLM / streamLLM, so they silently opted out of everything the shared
entry point applies:

- Usage accounting. recordUsage is reached only from the wrappers, so no
  instructor turn, evaluator pass, simulator line or narrator pass was ever
  accounted for - the highest-frequency, longest-context LLM traffic in the
  product. The comment at llm.ts:275, that every server-side call funnels
  through the wrappers, did not hold.
- The LLM_THINKING_DISABLED kill switch, read inside the wrappers only.
- One thinking resolution path. runtime-thinking.ts existed precisely because
  the agents bypassed the wrapper, and it was wired into instructor (x2) and
  evaluator but neither simulator site.

Also fix callLLM recording result.usage, which on a multi-step tool run
(stopWhen) is the final step alone; totalUsage is the aggregate, and the
instructor turn is exactly such a run. streamLLM already preferred the
aggregate.

Thinking semantics are preserved per call site: the teaching turns keep
force-disabling thinking, now through the wrapper's own `thinking` argument,
and keep their existing providerOptions spread, which injectProviderOptions
yields to; the simulator keeps honouring a per-request config, as before. The
one behavioural difference is on native adapters when no thinking config
arrives: the policy now resolves to an explicit lowest/disabled provider option
instead of leaving the model's default in place - which is what the policy
always claimed to do, and what OpenAI-compatible providers already got.
Unifying the wider divergence is a product decision and stays out of scope;
runtime-thinking.ts now documents it, along with the fact that the original
tool_choice justification no longer matches the tree.

A lint guard restricts importing generateText / streamText to lib/ai/llm.ts so
the split cannot grow back. It uses the @typescript-eslint variant of
no-restricted-imports deliberately: flat config replaces a rule's options per
key rather than merging them, and the base rule is already configured for the
lib/choreography and lib/video-export module boundaries.

The runtime-thinking test moves from asserting the retired helper to asserting
what the call site is responsible for, at the provider boundary: an evaluator
turn reaches the provider with a disabled config in the thinking store, does
not leak it, and is accounted for under its own usage source.

Closes #1003

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pbl-v2): address cross-review findings on the shared-entry migration

Consensus findings from an independent cross-vendor review of cc48ada.

callLLM recorded usage only after validation passed, so with `retries > 0`
every attempt that failed validation went unaccounted, and a result handed back
after the retries were exhausted recorded nothing at all. Each attempt that
reaches that point was billed, so record before validating. Latent today (no
caller passes retryOptions) but it sits in the function this PR already touches.

The simulator's character line now hands its incoming thinking config to
streamLLM instead of resolving providerOptions by hand. The hand-rolled call
covered native adapters only, so a stage-route thinking config was silently
dropped on OpenAI-compatible providers - the wrapper also seeds the thinking
context that their fetch wrapper reads. Native adapters resolve identically:
both paths end in the same normalizeProviderId + buildThinkingProviderOptions.

runtime-thinking.ts documents two things the previous docstring implied away.
On native adapters the constant is a no-op whenever a thinking config does
arrive, because the teaching turns pass their own providerOptions and
injectProviderOptions yields to a caller-set value. And "disabled" is resolved
against each model's catalogued capability, which for some models is not off:
measured against the current catalog, gpt-5.4-pro resolves to effort medium,
gemini-2.5-pro to thinkingBudget -1 (dynamic thinking) and claude-fable-5 to
adaptive/low. Those match the model's own default today, so behaviour is
unchanged, but the policy is weaker than its name suggests.

Tests cover both accounting paths (a retried attempt and an all-attempts-failed
run each record every attempt), and the defensive totalUsage fallback is now
labelled as defensive - ai@6 always reports the aggregate.

The lint guard's scope comment also notes namespace imports, which importNames
cannot see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pbl-v2): correct the thinking-policy rationale; stop tests writing the real usage log

Two things, both prompted by probing the live DeepSeek API with the instructing
turn's exact shape rather than reasoning from the tree.

The docstring in runtime-thinking.ts claimed the policy's original justification
had expired, on the grounds that `begin_turn` and the forced `tool_choice` it
used are gone. That reads as an invitation to delete the force-disable, and it
is wrong. Measured against DeepSeek V4 Pro:

  thinking on + tools + stopWhen  -> streams fine, 66 reasoning tokens
  thinking on + forced toolChoice -> still 400, "Thinking mode does not
                                     support this tool_choice"

The incompatibility is bound to a FORCED tool choice, not to tools in general.
Today's turns do not reach it, but anyone who reintroduces a forced tool choice
will, so the policy is a live guard rather than dead weight. It also buys
latency: the same probe reached its first token in 1.4s with thinking off
against 3.0s with DeepSeek's default thinking on, and the teaching turns gain
nothing from thinking while the instructor is the chattiest surface in the
product. The docstring now records the measurements instead of the guess.

Separately: a test run could append to the app's real usage log. Any test that
exercises callLLM / streamLLM reaches `recordUsage` through `recordUsageSafe`
without asking for it, so rows sourced `minimax-auth-test`,
`minimax-thinking-test`, `minimax-fixed-thinking-test` and `serialization-test`
were sitting in the live `data/usage/` file next to production traffic
(tests/ai/minimax-provider.test.ts:58,101,146 and
tests/ai/anthropic-serialization.test.ts:38 — neither predates this PR).
`recordUsage` now returns early under vitest unless the caller asked for a
specific `baseDir`, which is the fix at the boundary rather than in the two
tests that happened to trip it. Storage tests pass an explicit `baseDir` and
tests that assert accounting mock the module, so both keep working; verified by
running all three files and confirming the live file's line count is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(pbl-v2): drop the hardcoded thinking policy, finishing the unification

The point of this PR is that the PBL v2 runtime reaches the model the same way
everything else does. Keeping a hardcoded `PBL_V2_TEACHING_THINKING` override
inside the agents worked against that: the teaching turns still resolved
thinking through a second, private path, and it produced the awkward edges this
PR had to disclose — an override that silently loses to a caller-set
`providerOptions` on native adapters, and a "disabled" that resolves to
`effort: medium` or `thinkingBudget: -1` (dynamic thinking) on models whose
catalogued capability has no off value.

So the policy is gone. All five sites now pass the incoming request / stage-route
config straight to `streamLLM` / `callLLM`, which is the one place that resolves
provider options and seeds the thinking context. `runtime-thinking.ts` is
deleted, and the hand-rolled `resolveThinkingProviderOptions` calls with it.

What justified keeping it, and why that was not enough:

- The DeepSeek 400 is real — probed live, thinking on + a FORCED `toolChoice`
  still fails with "Thinking mode does not support this tool_choice" — but no
  call site in the tree forces a tool choice, so the policy was guarding a
  scenario that does not exist, at the cost of the entry-point unification. The
  hazard is now a comment where a forced tool choice would be introduced, which
  is where a future author will actually look.
- Thinking costs latency on the teaching turns (~1.4s to ~3.0s first token on a
  thinking-by-default model, measured on DeepSeek V4 Pro). That is a real cost
  but a deployment preference, and it belongs in the `pbl-v2-runtime` stage
  route, not hardcoded in the agent where it overrides whatever that route says.
  Noted at the instructor call site with the numbers.

Behaviour change to be aware of: on OpenAI-compatible providers the teaching
turns no longer force thinking off, so a thinking-by-default model now thinks
unless the route says otherwise — slower first token and reasoning tokens on the
bill. Pin `thinking` off on the stage route to keep the old behaviour.

The runtime-thinking test becomes runtime-llm-entry and asserts the new
invariant at the provider boundary: an incoming config arrives intact, NO config
means no config (a re-added constant would fail this), the global kill switch
still applies, nothing leaks, and the turn is accounted for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(pbl-v2): forward the resolved thinking config to the simulator narrator

Review finding from @cosarah on #1006, and a correct one: the director-narrator
pass was the last site still dropping the resolved config. Both callers passed
`thinkingConfig` into `runDirectorNarratorPass`, but the helper never destructured
it and never handed it to `callLLM`, so the provider silently fell back to the
model default instead of honouring the `pbl-v2-runtime:simulator` route.

Earlier revisions of this PR documented that as pre-existing and deliberately
untouched. That defence stopped holding once the hardcoded thinking policy was
deleted: the PR's invariant became "every PBL v2 site resolves thinking through
the shared entry point, and the deployment decides", and the narrator was the one
site contradicting it.

Coverage per the review: the simulator turn is now driven end to end against a
scenario fixture, asserting that BOTH provider calls — the streamed character
line and the non-streamed narrator pass — see the incoming config, that neither
substitutes one when the request carries none, and that both are accounted for
under their own usage sources. The `['generate','stream']` assertion keeps the
test from silently covering only one of the two calls.

Fault-injected to confirm it bites: dropping the new fourth argument again turns
"forwards an incoming config to the character line AND the narrator" red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(lint): close the dynamic-import bypass on the LLM entry-point guard

Second review pass from @cosarah on #1006. He is right that the guard was not
airtight, though the picture turned out narrower than described — I checked each
form against eslint instead of reasoning about it:

  import { streamText } from 'ai'   already blocked
  import * as ai from 'ai'          already blocked (ESLint reports a namespace
                                    import when importNames is set)
  require('ai')                     already blocked, by the repo-wide
                                    @typescript-eslint/no-require-imports
  await import('ai')                NOT blocked  <- the real hole
  await import(`ai`)                NOT blocked

So the dynamic form is now covered by a no-restricted-syntax block, in both the
literal and template-literal shapes. That rule key is configured by five earlier
blocks and flat config replaces options per key rather than merging them, so the
new block explicitly ignores those directories rather than silently dropping
their module boundaries. It costs nothing: lib/choreography and lib/video-export
already allowlist their import sources (importing 'ai' there is an error today,
and video-export bans ImportExpression and require outright), and the two
@openmaic packages are built in isolation against @openmaic/dsl with the SDK
nowhere in their dependency graph.

The comment above the guard claimed namespace imports were out of scope because
importNames cannot see them. That was wrong; it now lists what each form is
caught by, and notes that an eslint-disable still defeats all of it — which is
the point, since the bypass then has to be written down where a reviewer sees it.

Verified with a probe file using all five shapes: five errors, one per line.
Injecting `import 'react'` plus an `'ai'` import into lib/choreography still
fails on both pre-existing boundary rules, confirming no clobbering.

Also updates the PR description, which still described the narrator as ignoring
its thinking config — stale since 9bbb720.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(lint): cover the @openmaic packages with the dynamic-import ban

Third review pass from @cosarah on #1006, and he is right again: the guard
ignored packages/@openmaic/renderer and packages/@openmaic/storage, and
`void import('ai')` under the renderer source path passed lint. Reproduced before
fixing.

The previous revision justified those two exclusions by arguing the packages are
built in isolation against @openmaic/dsl with the SDK nowhere in their dependency
graph. That is an argument about why a bypass would not matter, not a rule that
stops one — exactly the kind of reasoning this PR exists to replace with
enforcement.

The two selectors are now a shared AI_SDK_DYNAMIC_IMPORT_BAN spread into both
package blocks, rather than dropping those directories from the repo-wide block's
ignores: they configure no-restricted-syntax for their own `@/` boundary, and
flat config replaces rule options per key, so matching their files would have
silently deleted that boundary.

lib/choreography and lib/video-export stay ignored and stay covered — their
blocks ban EVERY ImportExpression outright, which subsumes this one. That claim is
now checked rather than asserted.

Verified with a `void import('ai')` probe in six production paths — both
@openmaic packages, choreography, video-export, lib/pbl, app/api — one error
each; lib/ai/llm.ts and tests/ stay clean; and an `@/lib/foo` import under the
renderer still fails its own host-path boundary, confirming no clobbering.

Also rewrites the PR description's e2e section, which still described the deleted
thinking policy. The browser run predates that deletion, so its streaming and
accounting evidence stands (the wrapper path is unchanged) but its thinking
observations do not. Replaced with a fresh live probe of the final behaviour: with
no route thinking the instructing turn reports 60 reasoning tokens, with `thinking`
pinned off on the stage route it reports 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(lint): guard every source extension, and pin the coverage matrix in a test

Fourth review pass from @cosarah on #1006. Confirmed before fixing: an
`app/api/**/route.js` containing both a named and a dynamic `'ai'` import passed
lint with exit code 0, because both guard blocks matched only `**/*.{ts,tsx}`
while every module-boundary block in the same file matches
`**/*.{ts,tsx,js,jsx,mjs,cjs}`. Both blocks now match the same set. The repo has
real `.mjs` under scripts/, so this was not hypothetical.

That is the third scope hole review has found in this guard — two @openmaic
package directories, then the extension list — and each one was closed and then
verified by hand, which is the process that produced the next one. So the matrix
is now an executable contract: tests/lint-llm-entry-guard.test.ts runs the real
eslint against the real config over every import form (named, namespace, dynamic,
dynamic-template) × every source extension × nine production paths, asserts the
three deliberate exemptions still pass, and asserts the module boundaries that
share the `no-restricted-syntax` key still fire. A future edit that narrows
`files`, adds an `ignores` entry or swaps the rule key fails CI instead of
quietly reopening the door.

Fault-injected against both earlier findings to prove it bites: narrowing the
scope back to ts/tsx turns the js/jsx/mjs/cjs rows red, and re-adding the two
@openmaic packages to `ignores` turns the dynamic-import rows red. It also treats
an eslint-ignored path as "not covered", so a path that silently stops being
linted cannot masquerade as a pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 14:11:40 +08:00
wyucand杨慎 28ea48bba9 fix(storage): omit undefined object fields at persistence boundaries (#992)
* fix: canonicalize optional persistence fields

* fix: preserve document store method receivers

* fix: support immutable document stores

* fix: align persistence equality checks

* test: reset document store wrappers

* fix: normalize injected migration stores

---------

Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-07-26 23:45:43 +08:00
3eea9dc542 feat(runtime): complete the #869 learner-data cutover — quiz + playback onto RuntimeStore (#955)
* feat(runtime): persist quiz attempts in RuntimeStore

* fix(runtime): coalesce quiz draft snapshots

* fix(runtime): recover concurrent quiz attempts

* fix(runtime): handle quiz completion races

* fix(runtime): commit quiz review atomically

* fix(runtime): dedupe concurrent quiz writes

* fix(runtime): drop stale quiz drafts

* fix: harden quiz runtime review recovery

* fix: serialize quiz attempt identity

* feat: read quiz state from runtime store

* fix: persist quiz retries before resetting

* fix: preserve authoritative quiz outcomes

* fix: preserve legacy quiz retries during cutover

* fix: reconcile legacy quiz snapshots safely

* fix: drain rollover quiz write queues

* fix: drain completed quiz retry queues

* fix: reuse concurrent quiz retries

* fix(quiz): preserve drafts across abrupt reloads

* fix(quiz): recover empty retry sessions

* fix(chat): abort stalled runtime state reads

* fix(quiz): expose queued phases to readers

* fix(quiz): retain concurrent writer tails

* fix(quiz): canonicalize retry branches

* fix(quiz): keep retry rollovers monotonic

* fix(quiz): validate skipped retry siblings

* fix(quiz): close read cutover races

* test(classroom): cover legacy quiz summaries

* fix(quiz): reset async consumers on scene changes

* fix(quiz): close scene transition windows

* test(pbl): cover launch freshness guards

* fix(quiz): close cutover concurrency gaps

* fix(quiz): harden retry and context freshness

* fix(quiz): reject malformed legacy answers

* fix(quiz): validate legacy answer values

* test(quiz): cover legacy multi-answer migration

* fix(merge): retire dead ChatRequestTemplate.storeState after quiz read cutover

Main's three call sites built static storeState blocks that runAgentLoopFn
never consumed (it always rebuilds fresh state via getStoreState); the quiz
read cutover replaced that callback with the async two-phase RuntimeStore
read, leaving the template field with zero consumers. Drop it.

* feat(storage): conform HTTP/PG backends and reference server to RuntimeAppendOptions

The quiz write path's expectedLastSeq / sessionTransition / RuntimeAppendConflictError
semantics existed only in the browser backend; server-backed deployments would
silently accept conflicting appends and leave completed sessions active. Forward
the options over the wire, detect conflicts atomically under the PG transaction,
map them to HTTP 409 RUNTIME_APPEND_CONFLICT, and rematerialize the typed error
client-side so quiz retry logic works across every backend.

Co-authored-by: Codex <codex@openai.com>

* fix(chat): Pi single requests build storeState via the async runtime quiz read

Pi bypasses runAgentLoop's per-iteration getStoreState and serializes the
request template straight to /api/chat/pi, which rejects bodies without
storeState. Extract the fresh-snapshot builder (async RuntimeStore quiz read
with the scene-transition guard) and call it on the Pi path too.

* ci: whitelist the runtime-data-cutover integration trunk for PR checks

* feat(runtime): playback cutover — cursor in KV, discussion facts in RuntimeStore (#956)

* feat(runtime): cut playback over to the runtime layer — cursor in KV, facts in RuntimeStore (#869)

The fourth and last runtime family. Consumed-discussion facts become
append-only 'playback' records folded into a set at read (at-least-once
appends, no conflict machinery); the resume cursor is device-scoped
last-write-wins KV per the amended #779/#869 split. sessionStorage keeps
same-tab priority; KV takes over on fresh tabs/reloads. The dead Dexie
playbackState machinery is retired, with a one-time lazy migration of any
legacy row (cursor half + facts half) before deletion, and stage deletion
now clears both the KV cursor and any unmigrated legacy row.

Co-authored-by: Codex <codex@openai.com>

* test(runtime): include playbackState in the stage-delete db mock

---------

Co-authored-by: Codex <codex@openai.com>

* fix(playback): persist discussion facts on every consumption path (#957)

* fix(playback): persist discussion facts on every consumption path (final-review P0+P1s)

- The engine now publishes a progress snapshot the moment a discussion is
  consumed (join / skip / unselected-agent auto-skip). onProgress otherwise
  fires before the discussion action executes and a discussion is the scene's
  last action, so the fact never reached persistence.
- Reads fold records across ALL playback sessions in the learner partition
  (mergeLearner deliberately preserves same-kind sessions from both keys).
- Legacy migration appends only not-yet-durable facts, so an interrupted
  migration resumes instead of dropping the tail.
- recordConsumedDiscussion reports durability; the component drops failed ids
  from its observed set so a later progress tick retries (at-least-once).

* test(e2e): live verification of the playback persistence chain

Seeds a deterministic stage straight into the Dexie DB, starts the lecture
via the canvas overlay, and asserts the full chain: discussion auto-skip
appends a discussionConsumed record to maic-runtime, the device cursor lands
in KV, and both survive a fresh browsing context (empty sessionStorage).

* refactor(playback): consumed-discussion state is volatile by decision — cursor-only persistence (#959)

Product ruling on #869's fourth family: playback learner state is front-end
ephemeral UX, not learner data. A re-shown proactive card auto-skips, joined
discussions' content already lives in chat runtime records, and no replay
export / analytics consumer exists — so durable facts bought nothing over
in-memory + same-tab sessionStorage. Drop lib/playback/runtime.ts and the
RuntimeStore facts wiring; keep the device-scoped KV resume cursor (the half
with real UX value), the engine's consumption-time progress snapshot (cursor
freshness), and the legacy Dexie retirement (cursor half migrates, row
deletes, consumed ids are dropped).

* fix(review): P3 pair from cross-review — scene-id boundary + sessionTransition 400 (#966)

* fix(review): scene-id boundary for quiz context + 4xx for malformed sessionTransition (P3 pair)

Review findings on #955: didActiveSceneRemainUnchanged compared the active
scene by object identity, so a store update reallocating the scene during
the async quiz read dropped the learner's graded answers from that turn's
request — the scene id is the real boundary. The records route now
classifies a malformed sessionTransition as a validation failure instead of
letting the store's throw surface as a 500.

* fix(playback): superseded-engine cursor guard + migration write-window recheck

Second-vendor review of the #959 shrink (requested after the cross-review
noted it had single-vendor coverage) found: an engine orphaned by a scene
switch during async lecture resume could pass the idle-only recheck, be
resurrected, and publish its old scene's progress over the new scene's
debounced cursor — the resume continuation now requires identity with the
installed engine, and onProgress drops snapshots from superseded engines.
The legacy cursor migration also rechecks KV immediately before its write
so a concurrent tab's newer cursor cannot be overwritten and orphaned by
the legacy-row delete.

Co-authored-by: Codex <codex@openai.com>

---------

Co-authored-by: Codex <codex@openai.com>

* fix(review): approval follow-up P3 nits (#967)

* release: v0.3.1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): approval P3 nits — ISO gate on sessionTransition, dead mocks, corrupt-timestamp guard

- The records route's sessionTransition guard now requires an ISO
  updatedAt (isIsoTimestamp), matching the sibling PATCH /status route
- Dead vi.mock factories for the deleted playback-storage module dropped
- A corrupt legacy playback timestamp falls back to 'now' instead of
  wedging migration into a permanent re-throw that disabled resume

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:28:04 +08:00
wyuc 65bf20a84b feat(runtime): cut chat sessions over to RuntimeStore (#926)
* feat(runtime): persist chat sessions in RuntimeStore

* fix(runtime): harden chat persistence ordering

* fix(runtime): protect cross-tab chat snapshots

* fix(runtime): compare structured chat values safely

* fix(runtime): preserve chat backup and reset semantics

* fix(runtime): restore imported chat snapshots

* fix(runtime): isolate chat load failures

* fix(runtime): make cache clearing lossless

* fix(runtime): normalize empty chat saves

* fix(runtime): serialize legacy chat migration

* fix(chat): serialize backup restoration

* test(chat): cover multi-stage restore locking

* fix(chat): coordinate fallback migrations across tabs

* fix(chat): harden cross-tab migration locking

* fix(database): retire abandoned chat lease schema

* fix(chat): preserve monotonic local edits

* fix(chat): protect unseen runtime sessions

* fix(chat): order lifecycle transitions monotonically

* fix(chat): serialize runtime maintenance

* fix(chat): order queued writes before maintenance

* fix(chat): preserve cross-version lock compatibility

* fix(chat): preserve streamed and backup data

* fix(chat): isolate empty no-lock saves

* fix(chat): isolate partial runtime snapshots

* fix(runtime): close cross-store cutover races

* fix(runtime): close remaining cutover races

* fix(chat): retain caller-visible conflict baseline

* fix(chat): preserve read-only legacy autosaves

* fix(chat): avoid restore lock inversion

* fix(runtime): enroll writes before maintenance

* fix(runtime): retain maintenance ordering

* fix(runtime): close restore and deletion races

* fix(chat): order partition queues within lock epochs

* fix(chat): preserve recovery and deletion intent

* fix(runtime): recover interrupted chat restores

* fix(backup): scope chat deduplication by stage

* fix(backup): stage chats by runtime partition

* fix(backup): clear staged chats with stages
2026-07-16 18:30:40 +08:00
wyucand杨慎 e601eaa3d5 feat(pbl): cut learner state over to RuntimeStore (#922)
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-07-14 19:37:38 +08:00
dd83ac8669 feat(pbl): read flip — fold learner state from RuntimeStore with snapshot backfill (#869 Part C, step 3) (#902)
* feat(pbl): read flip — fold learner state from RuntimeStore with snapshot backfill (#869 Part C, step 3)

Step 3 of #869 Part C: on stage load, PBL learner state is reconstructed
by folding RuntimeStore records over the design-time template. Records
are made self-sufficient at drain time (id-only outbox events gain
content attachments resolved from the in-hand project), pre-existing and
ring-evicted history is recovered through snapshot records, and the
document stays authoritative whenever the fold disagrees — with a
self-heal snapshot so the next load converges. The #893 dual-write and
document persistence are untouched; removing them is the next step.

- lib/pbl/v2/runtime/learner-state.ts: PBLLearnerState split with
  extract/apply/strip — codifies the document/runtime boundary
- lib/pbl/v2/runtime/record-payloads.ts: enriched event, engagement and
  snapshot record payload shapes (app-owned, versioned)
- lib/pbl/v2/runtime/fold.ts: seq-ordered, id-deduped, epoch-aware fold
  with gap diagnostics; snapshots supersede earlier gaps
- lib/pbl/v2/runtime/hydration.ts: catch-up drain, fold, shadow compare,
  document-authoritative fallback, deduped self-heal snapshots; never
  throws into the stage load path
- thread_compacted runtime event: instructor memory compression was a
  learner-state mutation with no event; deterministic id from the
  post-compaction thread shape
- equivalence property test: randomized reducer runs into a real
  BrowserRuntimeStore must fold back to extractLearnerState(project)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): harden read-flip fold and hydration after cross-review

- remove thread_compacted: instructor compaction mutates a per-request
  server copy after the last streamed patch, so neither the compacted
  thread nor the event ever reached the persisted client document —
  earlierSummary stays extracted for forward-compat only
- fold: apply the shared engagement ring cap; clear gaps on
  project_reset (matching the snapshot branch); default-branch gap +
  compile-time exhaustiveness for future event kinds
- hydration: preserve document-only transient fields (runtimeEvents,
  pendingOpenTaskPriorQuizResults) on fold-match; lodash isEqual instead
  of JSON.stringify equality; hydrate scenes in parallel; classroom
  server-fallback load path now hydrates too
- stage load: re-check the in-memory guard after async hydration so a
  concurrent setScenes is not clobbered by the older disk snapshot
- drain: serialize overlapping drains per (stage, scene, learner) so
  duplicate records are not appended
- drop write-only anchor.recordSeq and the 'test' reason variant;
  shared attachment-required kind set; single clone helper; strict
  enriched-payload assertion; learner-state field inventory test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): close stale-load races and drain head-of-line blocking

- loadFromStorage bails when a different stage appears during the
  now-async hydration window, not just the same stage
- classroom server fallback re-checks stage identity after hydration
  before applying scenes
- each queued drain is individually time-bounded so a hung append
  cannot block the per-key drain chain forever
- unknown forward-version event kinds enrich into a valid payload
  (attachment null + reason) instead of a bare event

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): address review — no empty-scenes snapshot, truly serialized drains

- classroom fallback: hydrate first, then apply stage + scenes in the
  same synchronous update, so the debounced save can never persist
  { stage, scenes: [] } while hydration is in flight
- drain: the per-key chain now awaits the actual drain work (never the
  timeout race), and the work checks a cooperative deadline before each
  append and before watermark persistence — callers keep the bounded
  wait, overlapping same-key appends are impossible

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(store): only abort a load when navigation happens during hydration

The post-hydration guard compared the resident stage against the load
target, so a load started while another stage was legitimately resident
(direct loadFromStorage with no prior setStage) bailed out before the
legacy generationComplete self-heal ran. Snapshot the stage id at entry
and abort only when it changed to a different stage during the await.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): release a wedged same-key drain slot after a hard cap

The serialization chain linked on the raw work promise, so a single
appendRecord that never settles held the per-key slot forever. The
chain link now races the work against a hard cap (2x the drain
timeout): the cooperative deadline already prevents an expired drain
from appending further records or touching the watermark, so releasing
the slot lets the successor proceed while the worst case from the
straggler is one id-dedupable in-flight record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(store): serialize scene-committing loads with a monotonic request token

Guard predicates over resident stage ids cannot order overlapping
loads from the same origin (load C then load D from resident B — the
later navigation lost). Every async scene-committing load now claims a
shared monotonic token at entry and commits only while still the
newest request; setStage and clearStore advance the token so sync
navigation invalidates pending loads. The classroom fallback shares
the same counter, so both entry points order against each other. The
same-stage in-memory scenes recheck stays: generation's setScenes is
not a tokened load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(classroom): bind the server fallback to its navigation's load token

Claiming a fresh token at apply time laundered an old request into the
newest one: a stale /api/classroom response could overwrite a newer
navigation. The classroom page now claims one token at load start and
threads it through both the IndexedDB attempt and the server fallback;
the fallback helper only validates that origin token at commit.
loadFromStorage accepts the pre-claimed token, claiming internally for
existing callers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(classroom): cancel stale classroom loads across every async phase

The load-token guard only protected the scene commit; the rest of the
page load flow ran to completion for a superseded navigation, so a
stale run could merge the old stage's media tasks back into the shared
map, clear and repopulate the generated-agent registry over the newer
classroom, and drop the newer navigation's loading state. Extract the
orchestration into runClassroomLoad with an isCurrent() gate — the
effect-cleanup cancel flag combined with the load token — checked
after every await and before every shared-store write (restore, agent
save/load, settings, media kickoff, setError/setLoading). A superseded
run now performs zero further side effects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(classroom): do not invalidate the current load token when committing fallback scenes

The fallback commit went through setStage(), which claims a fresh
scene-load token — so a legitimate fallback load invalidated its own
token, the post-commit isCurrent() check then returned false, and the
guarded finally skipped setLoading(false), leaving the page stuck on
the loading screen (E2E never saw the sidebar/rail). Commit fallback
stage+scenes via a token-free setState (applyClassroomStageAndScenes)
instead. Stale loads are still blocked by the token checks before the
apply; only the load's own commit no longer cancels itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(classroom): resume generation for the current classroom regardless of load token

The auto-resume generation effect was gating generateRemaining /
markGenerationCompleteIfDone / generateMediaForOutlines on the
scene-load token (via activeLoadTokenRef). By the time the effect ran
the token had advanced, so generation for the freshly-mounted
classroom was silently skipped — a deck arriving from generation-
preview with pending scenes never finished, leaving the Edit-course
toggle disabled (the two slide E2E specs timed out clicking it).

Resuming generation for the mounted classroom is not a competing
scene-load; navigation away is already handled by the effect's own
cancelled flag (which also stops()). Gate resume on cancellation only.
The load token still guards scene-committing loads (runClassroomLoad,
fallback apply, loadFromStorage), so the review-round race fixes stay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(classroom): stop cancelling generation resume on benign effect re-runs

The generation-resume effect gained a cancelled-flag guard around
generateRemaining / markGenerationCompleteIfDone / generateMediaForOutlines.
generateRemaining has a new identity every render (useSceneGenerator's
useCallback depends on the options object, and the classroom passes a
fresh { onComplete } literal each render), so the effect re-runs on
every render and its cleanup trips cancelled. The kickoff runs inside an
async loadImageMapping().then(...), which resolves after a re-run has set
cancelled, so generation was silently skipped — the deck never left
'Generating...', the Edit-course toggle stayed disabled, and two slide
E2E specs timed out.

Restore the generation-resume effect to its prior unguarded form
(matches origin/main): navigation-away already stops the generator via
the load effect's stop() cleanup. The scene-load-token guards on the
actual load/commit paths (runClassroomLoad, fallback apply,
loadFromStorage) are untouched, so the review-round races stay fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(classroom): make generated-agent replacement atomic

* fix(classroom): clean up stale restored media

* fix(pbl): harden learner-state boundary

* fix(pbl): serialize hydration side effects

* fix(pbl): bound hydration drain barrier

* fix(pbl): bound queued hydration drains

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-07-14 16:31:39 +08:00
wyucandClaude Fable 5 5627b99156 feat(pbl): runtime-event outbox + dual-write into the RuntimeStore (#869 Part C, step 2) (#893)
* feat(pbl): emit typed runtime events from every learner-state mutation (#869)

The outbox half of the runtime split: pure operations append typed
PBLRuntimeEvents to project.runtimeEvents (ring-capped at 500, dedup by
id) for every learner-state mutation — messages, submissions,
evaluations, status changes across project/milestone/microtask/uiPhase,
handover and task-completion gates, and proficiency updates. Emission
stays inside the pure mutators (no I/O), so events cross the stateless
server boundary inside the project; a client-side drainer (next commit)
copies them into the RuntimeStore. Zero read-side changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(pbl): drain the runtime-event outbox into the RuntimeStore (#869)

Client-only drainer: after each persisted project change, copy new
runtimeEvents into the learner's active 'pbl' RuntimeSession (created
lazily), watermarked per stage through the device-scoped KV. Mid-drain
failures persist the last-success watermark and retry next tick; a
watermark evicted from the ring-capped ledger redrains visible events
(documented at-least-once — downstream folds dedup by event id). The
whole drain is bounded and never throws into the save path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(pbl): dual-write engagement events; end-to-end outbox coverage (#869)

The drainer now copies both in-project ledgers — runtimeEvents and
engagementEvents — into the learner's pbl RuntimeSession, with
independent watermarks that advance separately (a failure in one ledger
never loses the other's progress). End-to-end test drives real
operations through the reducers and drains into a real
BrowserRuntimeStore over fake-indexeddb, asserting the full ordered
event trail with correct anchors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): harden the outbox drain per cross-review (#869)

- Watermarks scope to (stageId, sceneId, learnerKey): multi-scene stages
  and multi-learner devices no longer cross-contaminate or redrain.
- First-session creation is race-free: deterministic session id
  (pbl-<stageId>-<learnerKey>) with already-exists fallback plus an
  in-flight memo, so concurrent drains cannot split one history.
- A corrupt watermark value degrades to a fresh redrain and is repaired
  on the next persist, instead of permanently blocking the drain.
- Stage deletion clears its drain watermarks alongside the runtime rows.
- resetProjectProgress emits a project_reset epoch marker so folds never
  resurrect pre-reset submissions/evaluations from the preserved ledger.
- Normalization repairs use deterministic event ids, collapsing echoes
  from cloned-copy re-normalization under id-dedup.
- Docs: ring-overflow latent-loss window (dual-write accepts it; the
  read-flip backfills from a projectV2 snapshot) and the fold-baseline
  contract (status 'active' / uiPhase 'hero' from design-time defaults).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): carry server-minted events across the advance patch (#869)

Two holes on the server-patch path: applyAdvanceProjectPatch computed
status-change 'from' values after Object.assign had already replaced the
milestone snapshot (suppressing every real Instructor-advance event as
from===to), and events minted server-side during advanceMicrotask (e.g.
handover_staged) lived only on the transient server copy — the advance
patch carried no runtimeEvents, so the client ledger never drained them.
Patches now carry the operation's event delta; the client appends them
first, and both sides mint deterministic patch:* ids for the same
transition so echoes collapse under id-dedup. Server-side facts are
authoritative; client emissions remain as a compatibility fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): scope deterministic event ids to the reset epoch (#869)

Deterministic norm:/patch: status-event ids now lead with the count of
project_reset events visible in the ledger, so a learner repeating the
same transition after a reset is recorded instead of deduped against the
pre-reset event. The epoch is derived from the same visible window the
id-dedup scans, so ring-buffer eviction can never desynchronize the two.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(store): drain debounced saves before teardown (#869)

setStageAgents schedules debouncedSave + debouncedSaveAgents; tests that
finished without flushing left timers firing after environment teardown,
lazily importing the settings/providers chain into a torn-down runtime
(deterministic on CI once this branch grew the stage-storage import
graph). Store tests now run fake timers and assert zero pending timers
at teardown; the same latent race in the insert-scene-after and
generation-complete suites is fixed in the same style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): address review — durable epoch, id-set deltas, ordered drain, post-save signal (#869)

- runtimeResetEpoch is a monotonic project field incremented on reset,
  never derived from the ring buffer, so evicting an old reset marker
  can no longer collapse post-reset transitions into pre-reset ids.
- Advance patches capture their event delta by id-set difference, not
  array index, so cap eviction mid-operation cannot drop server events.
- The drainer merges both ledgers into one timestamp-ordered stream
  (stable within each ledger) before appending, so RuntimeStore seq
  preserves global chronology instead of ledger-grouped order.
- Draining now subscribes to a post-save signal emitted after
  saveToStorage commits, instead of firing on the in-memory update —
  the dual-write can no longer outrun the source-of-truth persistence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pbl): drain the persisted snapshot, decoupled from scene mounting (#869)

The post-save signal now carries the PBL scenes exactly as persisted;
a module-level singleton subscription drains them, replacing the
component-ref hook. Navigating away before the debounced save fires no
longer strands the saved scene's outbox, and an in-flight save can no
longer drain a newer unsaved snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:12:53 +08:00
杨慎andwyuc 0f44e9a46f Add PBL v2 runtime APIs and classroom UI (#799)
* feat: add PBL v2 runtime UI

* fix: ship PBL v2 instructor avatar

* fix: ship PBL v2 workspace logo

* fix: prevent scenario PBL legacy fallback

* chore: restore package lint comments

* test: cover ordinary PBL legacy fallback

* fix: front-load PBL single-call validation

* fix: preserve PBL v2 content when assembling scenes

---------

Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn>
2026-06-26 11:56:02 +08:00
杨慎 09863af421 Add PBL v2 core schema and generation path (#795)
* feat: add PBL v2 core planner

* chore: restore unrelated lint comments

* fix: document PBL v2 scenario outline flags

* fix: expose PBL scenario subtype in outlines

* chore: clear PBL v2 completion stats lint warning

* chore: address PBL instructor code quality comments

* fix: address PBL v2 review findings

* chore: restore unrelated lint comments
2026-06-25 18:44:46 +08:00
wyucand杨慎 e613b7578f [codex] add per-model thinking config (#494)
* add per-model thinking config

* Refine thinking model controls

---------

Co-authored-by: 杨慎 <117187635+cosarah@users.noreply.github.com>
2026-04-26 19:41:10 +08:00
wyucandClaude Opus 4.7 f40c92f1aa refactor(orchestration): migrate prompt builders to file-based templates (#459)
* refactor(prompts): move loader to lib/prompts/

Move file-based prompt loader from lib/generation/prompts/ to
project-level lib/prompts/ so it can be shared by orchestration
and PBL prompts in subsequent commits.

- Templates and snippets moved alongside the loader
- getPromptsDir() now points at lib/prompts/
- Generation pipeline imports updated to @/lib/prompts
  (outline-generator, scene-generator, search-query-builder,
   scene-outlines-stream route)
- Add tests/prompts/loader.test.ts as sanity gate

Pure file move — no behavior change.

* test(orchestration): add baseline snapshots for prompt builders

Snapshot tests cover variant matrix:
- buildStructuredPrompt: role × scene-type × peer/ledger/discussion/profile
- convertMessagesToOpenAI: mixed message kinds
- summarizeConversation: truncation
- buildDirectorPrompt: Q&A vs discussion, ledger, profile
- buildPBLSystemPrompt: default config

These lock current output so the body refactor in subsequent
commits can be verified byte-equal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(orchestration): plug snapshot coverage gaps from review

- Split convertMessagesToOpenAI test: same-agent (existing) +
  cross-agent (new). The original test passed currentAgentId
  matching the message's agentId, so the cross-agent role-conversion
  branch was never exercised.

- Add 'teacher / quiz scene' snapshot to lock the spotlight/laser
  strip path in getEffectiveActions. The previous variant-7 test
  (whiteboard-open) only triggered the mutual-exclusion warning,
  not the strip — renamed it to reflect what it actually tests.

- Add assistant-role variant (was uncovered: ROLE_GUIDELINES,
  buildLengthGuidelines, buildWhiteboardGuidelines all branch on
  role).

* refactor(orchestration): extract summarizers from prompt-builder.ts

Move state-context, virtual whiteboard ledger replay, peer context,
message converter, and conversation summary out of the 890-line
prompt-builder.ts into focused modules under lib/orchestration/summarizers/.

prompt-builder.ts now only owns prompt assembly. director-graph
imports updated to the new locations.

Snapshot tests pass byte-equal — no behavior change.

* refactor(orchestration): move agent-system prompt to template file

The buildStructuredPrompt template literal is now
lib/prompts/templates/agent-system/system.md, assembled by a
thin variable-pass through the shared prompt loader.

Per-variant ternaries (format example, ordering, spotlight
examples, mutual-exclusion note) are kept as module-level
constants in prompt-builder.ts. ROLE_GUIDELINES,
buildLengthGuidelines, and buildWhiteboardGuidelines stay too
(they may further migrate to template partials in Phase 2).

Also: PROMPT_IDS gains a 'satisfies Record<string, PromptId>'
clause to prevent constant/union drift as more IDs are added.

Snapshot tests (12 prompt-builder + 4 director + 1 pbl) pass
byte-equal.

* refactor(prompts): align agent-system template with project conventions

- Rename agent-system placeholders snake_case → camelCase to match
  the convention already used by slide-actions/user.md and other
  generation templates. Settles convention before Task 5 lands two
  more templates that would otherwise inherit drift.

- Extract '## Speech Guidelines (CRITICAL)' into
  lib/prompts/snippets/speech-guidelines.md for reuse by director
  and PBL prompts in Task 5.

Snapshot tests pass byte-equal — placeholder name changes are
invisible in rendered output, and snippet inclusion happens at
load time.

* refactor(orchestration,pbl): move director + PBL prompts to templates

director-prompt.ts and pbl-system-prompt.ts now use the shared
template loader. Bodies collapse to thin variable assembly.

Three orchestration-tier prompts now share one infrastructure:
agent-system, director, pbl-design.

Snapshot tests pass byte-equal.

* refactor(orchestration): extract WhiteboardActionRecord + AgentTurnSummary to types module

These two interfaces are imported by 6+ modules including
summarizers/ — having them live in director-prompt.ts created
an awkward upward import direction (summarizers reaching back
to a sibling prompt builder for types).

Move them to lib/orchestration/types.ts and update all callers.
director-prompt.ts now imports them too.

Pure type-location refactor — snapshots pass byte-equal.

* test: remove snapshot tests in favor of eval-as-integration-test

Per review feedback: byte-equal snapshots were the wrong invariant
for this refactor. The goal was a stable substrate for editing
prompts, not a frozen contract. Snapshots would have generated
maintenance churn (~100-200 line diffs per intentional Phase 2
prompt tweak) without providing value the eval doesn't already.

Test posture going forward:
- tests/prompts/loader.test.ts (3 tests) — bounds the loader
  infrastructure (template load, snippet inclusion, variable
  interpolation, missing-id behavior)
- pnpm eval:whiteboard — bounds end-to-end agent loop, template
  composition, and integration with chat/director/state-manager

Removed: 1879 lines of .snap + 320 lines of test scaffolding +
fixtures. Net PR diff drops by ~2200 lines.

* docs(prompts): add README + structural assertion tests (review feedback)

Addresses two items flagged by PR #459 final review:

1. lib/prompts/README.md — conventions, syntax, gotchas, and local-testing
   recipe for template authors. Previously there was no doc for the stated
   goal of "non-engineers can edit prompts."

2. tests/prompts/templates.test.ts — 8 structural assertions covering:
   - no surviving {{...}} placeholders in any rendered template
   - role dispatch (teacher → LEAD TEACHER, student → not)
   - scene-type action stripping (quiz scene has no spotlight/laser)
   - director prompt output spec mentions next_agent

   These replace the removed byte-equal snapshot suite at much lower
   maintenance cost — they assert behaviors the refactor must preserve,
   not exact bytes.

* chore(prompts): relocate templates added on main during rebase

PR #461 (interactive mode clean) added 7 new templates under
lib/generation/prompts/templates/ (the pre-refactor location).
Rebase onto origin/main landed them at the old path; git's rename
detection didn't forward them through the directory move in db56c40.

Manually move them to lib/prompts/templates/ to match the new
convention, and fix scene-generator.ts's relative import
('./prompts/types' → '@/lib/prompts/types') since the dir no
longer exists as a sibling.

No behavior change — templates read from PROMPT_IDS values via
getPromptsDir() which already points at lib/prompts/.

* refactor(prompts): address review feedback from @cosarah

- loadSnippet now throws on missing file instead of silently returning
  a literal {{snippet:id}} string. A typo like {{snippet:speach-guidelines}}
  now fails at load time instead of reaching the LLM.

- README gains a "Still in TypeScript" section listing the role-conditional
  content that still lives in prompt-builder.ts (ROLE_GUIDELINES,
  buildLengthGuidelines, buildWhiteboardGuidelines) so contributors
  expecting a pure-markdown workflow know where to look.

- Expand tests/prompts/templates.test.ts to cover the conditional branches
  Phase 2 is most likely to touch (9 new assertions):
  - assistant role dispatch
  - peer-context section toggles on agentResponses presence
  - language constraint toggles on stage.languageDirective presence
  - director Q&A vs discussion mode branching
  - pbl-design {{issueCount}} substituted at all 3 occurrences
  - placeholder-naming-convention lint scans every template for
    non-camelCase placeholders (slide-content grandfathered)

- Comment above interpolateVariables regex documents why kebab-case
  placeholders pass through silently (the lint test now catches them).

- New test in loader.test.ts locks the throw-on-missing-snippet behavior.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:41:17 +08:00
杨慎andClaude Opus 4.6 470aa5e42e feat: inline language inference for outline and PBL generation (#412)
* feat: inline language inference in outline generation

Replace manual language selection with automatic inference from user
requirement text. The outline generator now produces a languageDirective
that propagates through the entire generation pipeline.

Key changes:
- Rewrite Language Inference section in outline prompt with decision
  rules for foreign language learning, cross-language PDF, proxy
  requests, and terminology handling
- Reorder pipeline: outlines before agents, so agent profiles can
  use the inferred languageDirective
- Pass languageDirective through scene content/actions generation
- Add SSE streaming of languageDirective from outline generation
- Remove manual language selection UI

Eval test suite:
- 50 test cases covering 9 scenario types: single-language, language
  learning, immersive, explicit instruction, code-switching, minimal
  input, user profiles, cross-language PDF, locale mismatch
- LLM-as-judge evaluation with configurable inference/judge models
- 50/50 pass rate with gemini-3-flash-preview + gpt-4o judge
- Excluded from CI (requires LLM API keys)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: remove useless attributes in test case

* feat: pass languageDirective to PBL, remove hardcoded zh-CN/en-US

- Remove pblConfig.language field, use languageDirective from outline
  inference instead
- Delete all Chinese prompt duplicates in pbl-system-prompt.ts,
  agent-templates.ts; keep English templates with languageDirective
  injection
- Remove zh-CN/en-US branches in generate-pbl.ts (initial prompt,
  post-process context, welcome message)
- Pipe languageDirective through scene-generator → generatePBLContent
  → IssueboardMCP → agent templates
- Remove i18n template wrapping from PBL welcome messages in
  pbl-renderer.tsx and use-pbl-chat.ts; use LLM-generated content
  directly to avoid language mismatch when UI locale ≠ course language
- Update pblConfig schema in outline prompt (remove language field)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: clean up legacy language fields, pass languageDirective through buildSceneFromOutline

- Add languageDirective param to buildSceneFromOutline, combining with
  outline.languageNote via buildLanguageText
- Remove dead types: AudienceProfile, StylePreferences, LegacyUserRequirements,
  SceneOutline.language
- Remove Stage.language and all its propagation (storage, stageInfo, prompt-builder fallback)
- Remove GenerateClassroomInput.language and job store inputSummary.language
- Remove unused pdfLanguageSample from outline SSE route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: persist languageDirective to IndexedDB, fix TTS preview and dead refs

- Add languageDirective to StageRecord and stage-storage write path
- Add DB v9 migration: convert legacy language locale codes to directives
- Use voice.language for TTS preview text instead of dead localStorage key
- Omit empty ## Language section in PBL agent prompts
- Use JSON.parse for extractLanguageDirective unescape (\uXXXX support)
- Remove dead i18n keys: toolbar.languageHint, pbl.chat.welcomeMessage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:49:56 +08:00
wyucandClaude Opus 4.6 843d33e5cc chore: enforce Prettier formatting and fix lint issues
- Add .prettierignore to exclude vendor packages, lock files, markdown, and YAML
- Update .prettierrc: printWidth 100, singleQuote, trailingComma "all"
- Run Prettier across all source files for consistent formatting
- Fix unused imports (UserRequirements, setTTSProvider)
- Fix eslint-disable comment placement after Prettier reformat

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:09:45 +08:00
wyucandClaude Opus 4.6 0d20abfed9 Initial commit: OpenMAIC AI classroom platform
Next.js 16 App Router application with:
- AI-powered interactive classroom generation from PDF/text requirements
- Multi-agent discussion system (teacher, students, assistant roles)
- Provider abstraction for LLM, TTS, ASR, image, video, web search
- Local-first data architecture (IndexedDB/Dexie)
- i18n support (zh-CN/en-US)
- Structured logger, SSRF guard, lint/build passing (0 errors)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 16:41:07 +08:00