Files
OpenMAIC/lib/prompts
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
..

lib/prompts

File-based prompt loader + templates shared by both the generation pipeline and the runtime orchestration layer.

Directory layout

lib/prompts/
├── loader.ts             ← file I/O + cache
├── index.ts              ← public API (loadPrompt, buildPrompt, …) + PROMPT_IDS
├── types.ts              ← PromptId / SnippetId string literal unions
├── templates/
│   └── <prompt-id>/
│       ├── system.md     ← required
│       └── user.md       ← optional (mostly for offline generation prompts)
└── snippets/
    └── <snippet-id>.md   ← reusable blocks referenced via {{snippet:…}}

Template syntax

Three kinds of placeholder:

Syntax Semantics Resolved by
{{variableName}} Value is provided by the caller via buildPrompt(id, vars) interpolateVariables in loader.ts
{{snippet:snippet-name}} File content is spliced in at load time processSnippets in loader.ts
{{#if conditionName}}...{{/if}} Content is included only when conditionName is truthy in the template variables processConditionalBlocks in loader.ts

Processing order is snippet includes first, then conditional blocks, then variable interpolation, so snippets may themselves contain {{#if}} blocks and {{variableName}} placeholders if the caller provides the value.

Conditional blocks read from the same variables record passed to buildPrompt — no separate conditions object is needed.

Naming conventions

  • Placeholder names use camelCase. Example: {{agentName}}, {{stateContext}}.
  • Template IDs use kebab-case. Example: agent-system, slide-content.
  • lib/prompts/templates/slide-content/{system,user}.md still uses legacy snake_case placeholders ({{canvas_width}}, {{canvas_height}}). This predates the camelCase convention; don't imitate it when writing new templates.

Adding a new prompt

  1. Create lib/prompts/templates/<new-id>/system.md (and user.md if needed).
  2. Add <new-id> to the PromptId union in types.ts.
  3. Add NEW_ID: '<new-id>' to the PROMPT_IDS constant in index.ts (the satisfies Record<string, PromptId> clause enforces that the value exists in the union).
  4. Call buildPrompt(PROMPT_IDS.NEW_ID, vars) from the consuming module.

Still in TypeScript (not yet in templates)

Not every prompt fragment lives in markdown. Some role-conditional content still exists as TS template literals and needs editing directly:

What Where Why not in markdown
ROLE_GUIDELINES (teacher / assistant / student blocks) lib/orchestration/prompt-builder.ts Branches by agentConfig.role
Length targets (100 / 80 / 50 chars per role) buildLengthGuidelines in lib/orchestration/prompt-builder.ts Branches by role

These may migrate into snippets in a later pass once Phase 2 eval feedback shows which parts need frequent iteration.

Silent-passthrough gotcha

interpolateVariables leaves unknown placeholders unchanged rather than throwing:

interpolate('hello {{missing}}', {}) === 'hello {{missing}}'

This is intentional for partial-render scenarios but means a typo in a placeholder name ships literal {{…}} text to the LLM. Defence:

  • Tests in tests/prompts/templates.test.ts assert that the fully-rendered agent-system / director / slide-content prompts contain no surviving {{…}} tokens. Keep that check passing when adding variables.
  • {{snippet:name}} lookups throw on a missing snippet file rather than passing through silently, so a typo like {{snippet:speach-guidelines}} fails at load time instead of reaching the LLM.

Testing a template change locally

The cheapest feedback loop is the template smoke suite:

pnpm test tests/prompts

For end-to-end runtime behaviour (agent loop + template composition + chat/director integration), use the whiteboard eval harness on one scenario:

PORT=3100 pnpm dev &
EVAL_CHAT_MODEL=<provider:model> EVAL_SCORER_MODEL=<provider:model> \
  pnpm eval:whiteboard --base-url http://localhost:3100 \
  --scenario econ-tech-innovation

Loading

loadPrompt and loadSnippet read from disk on every call. No caching — markdown edits take effect immediately without restarting any dev server. Prompt disk I/O is negligible next to the LLM call it feeds.