-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Expand file tree
/
Copy pathsystem.ts
More file actions
2076 lines (1916 loc) · 123 KB
/
Copy pathsystem.ts
File metadata and controls
2076 lines (1916 loc) · 123 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Prompt composer. The base is the OD-adapted "expert designer" system
* prompt (see ./official-system.ts) — a full identity, workflow, and
* content-philosophy charter. Stacked on top:
*
* 1. The discovery + planning + huashu-philosophy layer (./discovery.ts)
* — interactive question-form syntax, direction-picker fork,
* brand-spec extraction, TodoWrite reinforcement, 5-dim critique,
* and the embedded `directions.ts` library.
* 2. The active design system's DESIGN.md (if any) — palette, typography,
* spacing rules treated as authoritative tokens.
* 3. The active skill's SKILL.md (if any) — workflow specific to the
* kind of artifact being built. When the skill ships a seed
* (`assets/template.html`) and references (`references/layouts.md`,
* `references/checklist.md`), we inject a hard pre-flight rule above
* the skill body so the agent reads them BEFORE writing any code.
* 4. For decks (skillMode === 'deck' OR metadata.kind === 'deck'), the
* deck framework directive (./deck-framework.ts) is pinned LAST so it
* overrides any softer slide-handling wording earlier in the stack —
* this is the load-bearing nav / counter / scroll JS / print
* stylesheet contract that PDF stitching depends on. We also fire on
* the metadata path so deck-kind projects without a bound skill
* (skill_id null) still get a framework, instead of having the agent
* re-author scaling / nav / print logic from scratch each turn. When
* the active skill ships its own seed (skill body references
* `assets/template.html`), we defer to that seed and skip the generic
* skeleton — the skill's framework wins to avoid double-injection.
*
* The composed string is what the daemon sees as `systemPrompt` and what
* the Anthropic path sends as `system`.
*/
import { renderOfficialDesignerPrompt } from './official-system.js';
import { renderDiscoveryAndPhilosophy, renderSharedFramesBlock } from './discovery.js';
import {
PLATFORM_CONTRACTS_BLOCK,
PROMPT_INJECTION_RESISTANCE,
renderSlimCoreCharter,
SLIM_V2_ROLE_BOUNDARY_GUARD,
} from './core-slim.js';
import { renderDirectionIndexBlock, renderDirectionSpecBlock } from './directions.js';
import { DECK_FRAMEWORK_DIRECTIVE } from './deck-framework.js';
import { renderMediaGenerationContract } from './media-contract.js';
import { IMAGE_MODELS } from '../media/models.js';
import { renderPanelPrompt } from './panel.js';
import { defaultCritiqueConfig, type CritiqueConfig } from '@open-design/contracts/critique';
import {
executionProfileFromStreamFormat,
type ByokMediaDefaults,
type ChatSessionMode,
type ExecutionProfile,
type MediaExecutionPolicy,
type MediaSurface,
} from '@open-design/contracts';
// Prepended first in every composed prompt so it wins precedence over all
// later sections, including skill bodies and user/project instructions.
const ELEVENLABS_VOICE_PROMPT_OPTION_LIMIT = 100;
const ELEVENLABS_VOICE_OPTIONS_PROMPT_PREFIX = 'ElevenLabs voice list could not be loaded';
const SEMANTIC_OUTPUT_FILE_NAMES = `## Semantic output file names
For new user-facing deliverables, choose a short semantic project-relative filename derived from the user's brief, product, screen, or artifact type. Do not call every new artifact \`index.html\`.
Good examples: \`investor-pitch-deck.html\`, \`ai-community-pr-deck.html\`, \`refund-ops-dashboard.html\`, \`pricing-page.html\`, \`screens/ios-checkout.html\`, \`daily-digest.md\`, \`image-manifest.json\`.
When editing an existing artifact, preserve its existing filename unless the user asks for a copy or version. Use \`index.html\` only for fixed runtime conventions or a lightweight launcher/overview: live-artifact generated previews, HyperFrames compositions, static SPA/deploy entry mapping, plugin previews/examples, \`ui_kits/app/index.html\`, or a multi-screen overview that links to semantic screen files. If an active skill or template says to copy a seed to \`index.html\`, adapt the destination to a semantic filename unless the task is one of those fixed-path exceptions.`;
const PROMPT_SAFE_HTTP_STATUS_LABELS: Record<string, string> = {
'400': 'Bad Request',
'401': 'Unauthorized',
'403': 'Forbidden',
'404': 'Not Found',
'429': 'Too Many Requests',
'500': 'Internal Server Error',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
};
function renderUiLocalePrompt(
locale: string | undefined,
options?: { includeQuickBriefSamples?: boolean },
): string {
const normalized = locale?.trim();
if (!normalized || normalized.toLowerCase() === 'en') return '';
const languageName = normalized === 'zh-CN'
? 'Simplified Chinese'
: normalized === 'zh-TW'
? 'Traditional Chinese'
: normalized;
const lines = [
'# UI locale override',
'',
`The Open Design UI locale for this run is \`${normalized}\` (${languageName}). All user-visible chat prose and generated UI controls must follow this locale, especially \`<question-form>\` titles, descriptions, labels, placeholders, helper text, and option labels. Keep machine-readable ids and object option \`value\` fields exact and unlocalized.`,
`The artifacts you generate must also be in ${languageName}: every piece of user-visible copy in the HTML/React/page/deck you produce — headings, body text, navigation, button and link labels, captions, alt text, and form fields — is written in this language by default. This holds even when a chosen template, plugin, or design system ships its reference/example content in another language: treat that copy as a layout and style reference and translate/adapt it into ${languageName}, do not ship its wording verbatim. Keep brand names, code, and technical identifiers as-is, and honor an explicit user request for a different output language.`,
];
// The worked zh-CN quick-brief copy below matches the CLASSIC default
// discovery form verbatim. The slim charter recipes that form instead of
// reciting it, and its form contract already requires localizing every
// user-facing string — so slim drops the sample block rather than pinning
// agents to copy written for a form layout the prompt no longer carries.
if (normalized === 'zh-CN' && (options?.includeQuickBriefSamples ?? true)) {
lines.push(
'',
'For the default quick brief in Simplified Chinese, use copy like:',
'- title: `快速简报 — 30 秒`',
'- description: `开始生成前我会先确认这些信息。不适用的可以跳过,我会补上默认值。`',
'- output label/options: `我们要做什么?` / `幻灯片 / 路演稿`, `单页网页原型 / 落地页`, `多屏应用原型`, `数据看板 / 工具界面`, `编辑式 / 营销页面`, `其他 — 我来描述`',
'- platform label/options: `目标平台` / `响应式网页`, `桌面网页`, `iOS 应用`, `Android 应用`, `平板应用`, `桌面应用`, `固定画布 (1920×1080)`',
'- audience label/placeholder: `目标用户` / `例如:早期投资人、开发者工具采购者、内部高管评审`',
'- tone label/options: `视觉调性` / `编辑 / 杂志感`, `现代极简`, `活泼 / 插画感`, `科技 / 工具型`, `奢华 / 精致`, `粗野 / 实验性`, `人性化 / 亲切`',
'- brand label/options: `品牌背景` / `帮我选一个方向`, `我有品牌规范 — 稍后分享`, `参考网站 / 截图 — 稍后附上`',
'- scale label/placeholder: `大概需要多少内容?` / `例如:8 页幻灯片、1 个落地页 + 3 个子页面、4 个移动端界面`',
'- constraints label/placeholder: `还有什么需要知道的吗?` / `真实文案、必须使用的字体、需要避免的内容、截止时间…`',
);
}
return lines.join('\n');
}
function normalizePromptText(value: string): string {
return value
.replace(/[\r\n]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function formatElevenLabsVoiceOptionsErrorForPrompt(
error: string | undefined,
): string | undefined {
const trimmed = normalizePromptText(error ?? '');
if (!trimmed) return undefined;
if (/no ElevenLabs API key/i.test(trimmed)) {
return `${ELEVENLABS_VOICE_OPTIONS_PROMPT_PREFIX} because the ElevenLabs API key is missing. Tell the user to configure it in Settings or paste a voice id manually.`;
}
const statusMatch = trimmed.match(
/(?:\((\d{3})(?:\s+([^)]+))?\)|\b(\d{3})(?:\s+([A-Za-z][A-Za-z -]{0,40}))?\b)/,
);
if (statusMatch) {
const statusCode = statusMatch[1] ?? statusMatch[3];
const statusText = statusCode ? PROMPT_SAFE_HTTP_STATUS_LABELS[statusCode] ?? '' : '';
const suffix = statusText ? ` ${statusText}` : '';
return `${ELEVENLABS_VOICE_OPTIONS_PROMPT_PREFIX} (${statusCode}${suffix}). Tell the user to retry the lookup or paste a voice id manually.`;
}
return `${ELEVENLABS_VOICE_OPTIONS_PROMPT_PREFIX}. Tell the user to retry the lookup or paste a voice id manually.`;
}
type ProjectMetadata = {
kind?: string;
intent?: string | null;
fidelity?: string | null;
speakerNotes?: boolean | null;
slideCount?: string | null;
animations?: boolean | null;
includeLandingPage?: boolean | null;
includeOsWidgets?: boolean | null;
templateId?: string | null;
templateLabel?: string | null;
platform?: string | null;
platformTargets?: string[] | null;
inspirationDesignSystemIds?: string[];
skipDiscoveryBrief?: boolean | null;
examplePrompt?: boolean | null;
examplePromptTitle?: string | null;
examplePromptBrief?: Record<string, string> | null;
imageModel?: string | null;
imageAspect?: string | null;
imageStyle?: string | null;
videoModel?: string | null;
videoLength?: number | null;
videoAspect?: string | null;
audioKind?: string | null;
audioModel?: string | null;
audioDuration?: number | null;
voice?: string | null;
brandId?: string | null;
brandSourceUrl?: string | null;
brandDesignSystemId?: string | null;
promptTemplate?: {
id?: string | null;
surface?: 'image' | 'video' | null;
title?: string | null;
prompt?: string | null;
summary?: string | null;
category?: string | null;
tags?: string[] | null;
model?: string | null;
aspect?: string | null;
source?: {
repo?: string | null;
license?: string | null;
author?: string | null;
url?: string | null;
} | null;
} | null;
contextPlugins?: Array<{
id?: string | null;
title?: string | null;
description?: string | null;
}> | null;
contextMcpServers?: Array<{
id?: string | null;
label?: string | null;
transport?: string | null;
url?: string | null;
command?: string | null;
}> | null;
contextConnectors?: Array<{
id?: string | null;
name?: string | null;
provider?: string | null;
category?: string | null;
status?: string | null;
accountLabel?: string | null;
}> | null;
};
type ProjectTemplate = { name: string; description?: string | null; files: Array<{ name: string; content: string }> };
type AudioVoiceOption = {
name: string;
voiceId: string;
category?: string | null;
labels?: Record<string, string> | null;
};
type ExclusiveSurfaceMode = 'deck' | 'image' | 'video' | 'audio';
const EXCLUSIVE_SURFACE_MODES = new Set<ExclusiveSurfaceMode>(['deck', 'image', 'video', 'audio']);
export function resolveExclusiveSurface(args: {
metadata?: ProjectMetadata | undefined;
skillMode?: ComposeInput['skillMode'] | undefined;
skillModes?: ComposeInput['skillModes'] | undefined;
}): ExclusiveSurfaceMode | null {
const activeSkillModes = new Set(
Array.isArray(args.skillModes)
? args.skillModes.filter(Boolean)
: args.skillMode
? [args.skillMode]
: [],
);
const metadataSurface = EXCLUSIVE_SURFACE_MODES.has(args.metadata?.kind as ExclusiveSurfaceMode)
? args.metadata?.kind as ExclusiveSurfaceMode
: null;
const primarySkillSurface = EXCLUSIVE_SURFACE_MODES.has(args.skillMode as ExclusiveSurfaceMode)
? args.skillMode as ExclusiveSurfaceMode
: null;
const composedSurfaceModes = Array.from(activeSkillModes).filter((mode): mode is ExclusiveSurfaceMode =>
EXCLUSIVE_SURFACE_MODES.has(mode as ExclusiveSurfaceMode),
);
return metadataSurface
?? primarySkillSurface
?? (composedSurfaceModes.length === 1 ? composedSurfaceModes[0] ?? null : null);
}
// Deck-ish vocabulary across English and Chinese briefs. Kept deliberately
// generous: a false positive only re-injects the deck framework a freeform
// run would previously have received unconditionally, while a false negative
// means the agent hand-rolls deck scaffolding — so every borderline term
// stays in.
const DECK_INTENT_SIGNAL =
/\b(slides?|deck|keynote|presentation|pitch\s?deck|ppt(x)?|slideshow|carousel)\b|幻灯|简报|讲稿|演示|路演|汇报|宣讲|课件|讲解|演讲|提案/i;
/**
* Whether the outgoing user request reads as a slide-deck brief. Gates the
* ~20K maybe-deck framework injection for freeform (kind=other / no
* metadata) projects: those runs previously carried the full framework on
* every turn "just in case". Feed it USER-AUTHORED text only (see
* `extractUserAuthoredSignalText`) — assistant turns in a packed transcript
* offer deck vocabulary the user never chose; conversation persistence is
* the latch's job (`latchConversationIntentSignals`), not the scanner's.
* Callers that cannot supply the request text should pass undefined to
* `freeformDeckSignal`, which preserves the legacy always-inject behavior.
*/
export function detectDeckIntentSignal(
...texts: Array<string | null | undefined>
): boolean {
return texts.some(
(text) => typeof text === 'string' && DECK_INTENT_SIGNAL.test(text),
);
}
// Media-generation vocabulary across English and Chinese briefs. Same
// generosity policy as DECK_INTENT_SIGNAL: over-firing keeps the dispatch
// hint (status quo), under-firing only costs the ~1.4K hint until the user
// actually mentions media — at which point the transcript-scanned signal
// flips true for the rest of the conversation.
const MEDIA_INTENT_SIGNAL =
/\b(image|images|photo|picture|video|audio|music|voice(over)?|sound|illustration|logo|banner|poster|icon set|wallpaper|avatar|imagen|midjourney|flux|veo|sora|suno)\b|图片|图像|生成图|配图|插画|海报|壁纸|头像|视频|短片|音频|音乐|配音|音效|表情包/i;
// Platform vocabulary across English and Chinese briefs. Same generosity
// policy as the deck/media signals: over-firing injects a ~1K contracts
// block that classic carried unconditionally, so the failure direction is
// status quo; under-firing loses per-platform delivery detail.
const PLATFORM_INTENT_SIGNAL =
/\b(ios|iphone|ipad|android|tablet|responsive|mobile app|native app|desktop app|cross[- ]platform|multi[- ]platform)\b|移动端|手机端|安卓|苹果|平板|响应式|跨端|多端|双端/i;
/**
* Whether the visible conversation names a delivery platform. Backstops the
* metadata-based gate for PLATFORM_CONTRACTS_BLOCK: freeform projects with
* no platform metadata but a platform-explicit brief ("做个 iOS app 原型")
* still need the per-platform delivery contracts classic carried always-on.
*/
export function detectPlatformIntentSignal(
...texts: Array<string | null | undefined>
): boolean {
return texts.some(
(text) => typeof text === 'string' && PLATFORM_INTENT_SIGNAL.test(text),
);
}
/**
* Whether the visible conversation mentions generating media. Gates the
* MEDIA_DISPATCH_HINT for non-media projects: most runs never generate
* media, so the generate→wait dispatch hint only ships once the request
* text (transcript included) shows media vocabulary. Callers that cannot
* supply the request text pass undefined to `mediaHintSignal`, which
* preserves the legacy always-inject behavior.
*/
export function detectMediaIntentSignal(
...texts: Array<string | null | undefined>
): boolean {
return texts.some(
(text) => typeof text === 'string' && MEDIA_INTENT_SIGNAL.test(text),
);
}
// Genuine transcript turn boundaries are exactly these lines: the web
// transcript builder writes `## <role>` verbatim and escapes any interior
// look-alike line (`escapeTranscriptRoleDelimiters`,
// apps/web/src/providers/daemon.ts), so an unescaped marker line in a packed
// message is always a real boundary.
const TRANSCRIPT_USER_MARKER = '## user';
const TRANSCRIPT_ASSISTANT_MARKER = '## assistant';
const TRANSCRIPT_CONTEXT_WARNING_MARKER = '## context warning';
// A packed transcript (buildDaemonTranscript, apps/web/src/providers/
// daemon.ts) always starts with a role marker or the context-warning header,
// and always carries the latest user turn. Both properties are required
// before treating a message as a transcript: a plain prompt that merely
// QUOTES `## user` mid-text (the quote is not the first content line) must
// keep whole-text scanning — dropping the text before the quote would
// silence the very request the signals exist to detect.
function isPackedTranscriptShape(lines: string[]): boolean {
if (!lines.includes(TRANSCRIPT_USER_MARKER)) return false;
const firstContent = lines.find((line) => line.trim().length > 0);
return (
firstContent === TRANSCRIPT_USER_MARKER ||
firstContent === TRANSCRIPT_ASSISTANT_MARKER ||
firstContent === TRANSCRIPT_CONTEXT_WARNING_MARKER
);
}
// Mirrors the repo's accepted form-answer header grammar — the shared parser
// in packages/contracts/src/artifacts/od-card.ts (parseFormAnswers) accepts
// em-dash / hyphen / colon separators and the bare `[form answers]` header,
// case-insensitively; the CLI docs show the hyphen form. Narrowing must fire
// for every variant or CLI/manual form-answer turns re-enter the echoed-label
// false-positive path. Grammar parity is pinned by
// tests/prompts/intent-signal-user-text.test.ts.
const FORM_ANSWERS_HEADER = /^\s*\[form answers(?:\s*[—\-:]\s*[^\]]+)?\]\s*$/i;
const FORM_ANSWERS_ANSWER_LINE = /^\s*-\s+[^:]*:\s*(.*)$/;
// `formatFormAnswers` (apps/web/src/artifacts/question-form.ts) echoes each
// question as `- <question label>: <value>`. The label is the FORM's copy,
// not the user's words — the real task-type form carries "For slide decks,
// include speaker notes?" — so only the value part of each answer line may
// feed the intent scan. Whether a gated block is introduced is decided by
// what the user actually answered, not by what the form offered.
function narrowFormAnswerSignalText(body: string): string {
const lines = body.split('\n');
const firstContent = lines.find((line) => line.trim().length > 0);
if (!firstContent || !FORM_ANSWERS_HEADER.test(firstContent)) return body;
return lines
.map((line) => {
if (FORM_ANSWERS_HEADER.test(line)) return '';
const answer = FORM_ANSWERS_ANSWER_LINE.exec(line);
return answer ? answer[1] ?? '' : line;
})
.join('\n');
}
/**
* Reduce an outgoing request message to the text the USER actually authored,
* for intent-signal scanning. The three intent signals gate stable-region
* prompt blocks, and for transcript-resending clients `message` embeds the
* full packed conversation — assistant turns included. Assistant copy (most
* damagingly the default discovery form's own option copy: «幻灯 / 路演»,
* "Slide deck / pitch", «iOS / Android / 响应式») must never flip a signal:
* every flip changes the stable instruction hash and re-sends the whole
* stable block on resume.
*
* - Packed transcript (contains `## user` / `## assistant` marker lines):
* returns only the bodies of `## user` sections; `## assistant` sections
* and the leading `## context warning` block are dropped entirely.
* - Plain message (no role markers): returned unchanged (legacy whole-scan).
* - `[form answers — <id>]` blocks in either shape are narrowed to the value
* part of each `- label: value` line (see narrowFormAnswerSignalText).
*/
export function extractUserAuthoredSignalText(
message: string | null | undefined,
): string {
if (typeof message !== 'string' || message.length === 0) return '';
// Marker lines are compared with any trailing CR stripped so CRLF
// transcripts parse identically to the LF ones the web builder emits.
const lines = message.split('\n').map((line) =>
line.endsWith('\r') ? line.slice(0, -1) : line,
);
if (!isPackedTranscriptShape(lines)) {
return narrowFormAnswerSignalText(message);
}
const userSections: string[][] = [];
// null = dropped region: pre-marker text (`## context warning` included)
// and `## assistant` sections.
let currentUserSection: string[] | null = null;
for (const line of lines) {
if (line === TRANSCRIPT_USER_MARKER) {
currentUserSection = [];
userSections.push(currentUserSection);
continue;
}
if (line === TRANSCRIPT_ASSISTANT_MARKER) {
currentUserSection = null;
continue;
}
currentUserSection?.push(line);
}
return userSections
.map((sectionLines) => narrowFormAnswerSignalText(sectionLines.join('\n')))
.join('\n\n');
}
export const BASE_SYSTEM_PROMPT = renderOfficialDesignerPrompt('filesystem');
export const SKIP_DISCOVERY_BRIEF_OVERRIDE = `# Automated project mode — skip discovery form
This project was created through the daemon API with \`skipDiscoveryBrief: true\`. Override the discovery rules below: do NOT emit a project-opening \`<question-form id="discovery">\` or show "Quick brief — 30 seconds". Treat the user's first message and project metadata as the brief, then proceed directly to planning/building under the normal artifact workflow. Ask at most one concise follow-up only if a required detail is impossible to infer safely.`;
// Injected into non-media projects so the agent knows how to dispatch
// media generation if the user asks for it mid-session (e.g. "generate an
// image with fal"). Without this, agents in prototype/deck projects try to
// call provider REST APIs directly and ask the user for keys that the daemon
// already holds in .od/media-config.json.
// Kept deliberately compact: this hint ships on EVERY non-media project
// (the vast majority never generate media), so the worked generate→wait
// bash recipe lives in `od media help` (printMediaHelp in cli.ts) and the
// CLI's own stderr handoff guidance instead of the prompt. The hint only
// needs to (1) route the agent to the dispatcher instead of provider APIs,
// (2) state the handoff/exit-code semantics, and (3) pin the behavioral
// rules agents historically fumbled (PowerShell translation, jq, asking
// for API keys, substituting fal-ai/* model paths).
const MEDIA_DISPATCH_HINT = `
---
## Media generation (if asked)
If the user asks you to generate an image, video, or audio file — regardless of which provider or model they mention (fal, Replicate, OpenAI, etc.) — use the daemon dispatcher via your **Bash tool**. Do NOT call provider REST APIs directly.
The daemon injects these env vars into your shell (**POSIX bash — not PowerShell**):
- \`OD_NODE_BIN\` — absolute path to the Node runtime
- \`OD_BIN\` — absolute path to the OD CLI script
- \`OD_PROJECT_ID\` — the active project id
**Always use the generate→wait loop below.** \`media generate\` always exits 0 — either with \`{"file":{...}}\` if done within ~25s, or with \`{"taskId":"..."}\` as a handoff for slow models. Whenever the output contains a \`taskId\`, keep polling with \`media wait\` until exit 0 (done) or exit 5 (failed).
Use **POSIX \`$VAR\` syntax** — do NOT translate to PowerShell (\`$env:VAR\`, \`&\` operator). Uses \`python3\` for JSON parsing (do NOT use \`jq\`):
\`\`\`bash
# POSIX bash — do NOT convert to PowerShell
IMAGE_MODEL=IMAGE_MODEL_VALUE
out=\$("$OD_NODE_BIN" "$OD_BIN" media generate \\
--project "$OD_PROJECT_ID" \\
--surface image \\
--model "$IMAGE_MODEL" \\
--prompt "..." \\
--aspect 16:9)
ec=\$?
if [ "\$ec" -ne 0 ]; then echo "\$out" >&2; exit "\$ec"; fi
last=\$(printf '%s\\n' "\$out" | tail -1)
task_id=\$(printf '%s\\n' "\$last" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('taskId',''))" 2>/dev/null)
since=\$(printf '%s\\n' "\$last" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('nextSince',0))" 2>/dev/null)
since="\${since:-0}"
while [ -n "\$task_id" ]; do
out=\$("$OD_NODE_BIN" "$OD_BIN" media wait "\$task_id" --since "\$since")
ec=\$?
last=\$(printf '%s\\n' "\$out" | tail -1)
since=\$(printf '%s\\n' "\$last" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('nextSince',\$since))" 2>/dev/null)
since="\${since:-0}"
if [ "\$ec" -eq 0 ]; then
task_id=""
elif [ "\$ec" -ne 2 ]; then
echo "\$out" >&2; exit "\$ec"
fi
done
printf '%s\\n' "\$last"
\`\`\`
The command exits \`0\` with one line of JSON: \`{"file":{...}}\` when done within ~25s, or \`{"taskId":"..."}\` as a SUCCESSFUL handoff for slow models. On a handoff, run the exact \`media wait\` command the CLI prints on stderr and repeat it until exit \`0\` (done) or exit \`5\` (failed); exit \`2\` means still running — not a failure. Parse JSON with \`python3\`, never \`jq\`.
MODEL_SELECTION_GUIDANCE`;
function renderByokMediaDefaultsHint(defaults?: ByokMediaDefaults): string {
const lines: string[] = [];
const imageModel = defaults?.imageModel?.trim();
const videoModel = defaults?.videoModel?.trim();
const speechModel = defaults?.speechModel?.trim();
const speechVoice = defaults?.speechVoice?.trim();
if (imageModel) lines.push(`- Image model: \`${imageModel}\``);
if (videoModel) lines.push(`- Video model: \`${videoModel}\``);
if (speechModel) lines.push(`- Speech model: \`${speechModel}\``);
if (speechVoice) lines.push(`- Speech voice: \`${speechVoice}\``);
if (lines.length === 0) return '';
return `
### Run-scoped BYOK media defaults
The user selected these BYOK media defaults in the chat UI for this run. Use
them when dispatching media unless the current user message explicitly asks for
a different model or voice.
${lines.join('\n')}`;
}
function shellDoubleQuote(value: string): string {
return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
}
function renderMediaDispatchModelGuidance(defaults?: ByokMediaDefaults): string {
const imageModel = defaults?.imageModel?.trim();
const videoModel = defaults?.videoModel?.trim();
const imagePart = imageModel
? `For image generation prefer your configured model: \`${imageModel}\`.`
: 'For the best fal image model use `--model flux-pro-ultra`.';
const videoPart = videoModel
? `For video prefer your configured model: \`${videoModel}\`.`
: 'For video use `--model veo-3-fal` or `--model wan-2.1-t2v`.';
return `${imagePart} ${videoPart} Always pass \`--surface\` explicitly (\`image\`, \`video\`, or \`audio\`). Any \`fal-ai/*\` path (e.g. \`fal-ai/flux/schnell\`, \`fal-ai/wan-i2v\`) is also a valid \`--model\` value for image/video — pass it through as-is without substitution.`;
}
function renderMediaDispatchHint(defaults?: ByokMediaDefaults): string {
const imageModel = defaults?.imageModel?.trim() || 'flux-pro-ultra';
const hint = MEDIA_DISPATCH_HINT
.replace('IMAGE_MODEL_VALUE', shellDoubleQuote(imageModel))
.replace('MODEL_SELECTION_GUIDANCE', renderMediaDispatchModelGuidance(defaults));
return `${hint}${renderByokMediaDefaultsHint(defaults)}`;
}
const FILESYSTEM_HANDOFF_OVERRIDE = `
---
## Filesystem handoff
This run uses Open Design's filesystem execution profile. Project files are the source of truth for generated artifacts.
Normal rhythm for artifact work:
1. Start with a short ordinary assistant message or compact \`<od-card>\` that states the locked direction.
2. Use progress tools for planning/status.
3. Create or edit project files through the runtime's native tool-call interface.
4. End with a short ordinary assistant message naming the written file(s) and summarizing the result.
Never type a tool invocation into assistant text as XML, markdown, JSON, or prose; if the runtime cannot call the tool, briefly explain that instead of simulating it.
This tool-call rule does not apply to Open Design UI markup. \`<question-form>\` and \`<od-card>\` are assistant text blocks that the host renders in the UI, not tool calls. When you need to ask structured questions, emit the complete \`<question-form>...</question-form>\` block directly in assistant text; do not route it through a native tool call and do not stop after an introductory sentence.
When you write or edit an HTML file in the project folder through the native file tool, that file is already visible in the user's file panel and preview.
- Do not output generated source code in a \`<artifact type="text/html">...</artifact>\` block.
- Do not duplicate file contents in assistant text after writing them to disk.
- After the final self-check, briefly name the written file and summarize the result instead.
- A filesystem run that emits a source-code \`<artifact>\` is treated as an unexpected fallback by the host.`;
export function buildExamplePromptOverride(
title?: string | null,
brief?: Record<string, string> | null,
): string {
let text = `# Example prompt mode — full-quality direct generation
The user selected a curated example prompt from the gallery and sent it without modification. This prompt is a complete, self-contained creative brief that has been carefully designed to produce a showcase-quality artifact.`;
if (title) {
text += `\n\nSelected example: "${title}"`;
}
if (brief && Object.keys(brief).length > 0) {
text += `\n\nPre-filled creative brief (treat as if the user already answered all discovery questions):`;
for (const [key, value] of Object.entries(brief)) {
text += `\n- ${key.replace(/_/g, ' ')}: ${value}`;
}
}
text += `\n\nRules:
1. Do NOT emit \`<question-form id="discovery">\`, do NOT show "Quick brief — 30 seconds", and do NOT ask any clarifying questions.
2. Treat the user's message as the FULL specification — it contains all visual direction, content themes, and structural intent needed.
3. Generate the artifact at your absolute highest quality. This is a showcase piece — match or exceed the standard of a hand-crafted design.
4. Infer any unspecified details (copy, layout choices, imagery descriptions) in a way that is maximally coherent with the stated creative direction.
5. Proceed directly to planning and building. Output your TodoWrite plan and then the artifact immediately.`;
return text;
}
const ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE = `
---
## Active design system visual direction
Active design system exception: the active design system is the visual direction for this project. Use its DESIGN.md palette, typography, spacing, component rules, and theme tokens as the source of truth for color and mood.
- Do not ask the user to pick a separate theme color, visual direction, palette, typography mood, or direction card.
- Do not emit a direction question-form, a \`direction-cards\` picker, or any visual-direction card while an active design system is present.
- If an earlier discovery answer asks to "Pick a direction for me", treat that as already satisfied by the active design system and continue with the plan.
- When a downstream framework mentions "active direction" or "theme tokens", bind those fields from the active design system instead of the built-in direction library.
`;
const DEFAULT_DESIGN_SYSTEM_USAGE = `Read DESIGN.md for visual principles, paste tokens.css verbatim into the first <style> when it is provided, and match component shapes from the reference component manifest or fixture when available. Treat any pull-layer index as optional context for deeper inspection; do not assume those files have already been loaded.`;
function renderDesignSystemImportModeGuidance(
importMode: ComposeInput['designSystemImportMode'],
): string | undefined {
if (importMode === 'normalized') {
return 'This package is normalized. Treat tokens.css and DESIGN.md as the contract, and prefer OD token names over source-project names. Use pull-layer source evidence only as optional background.';
}
if (importMode === 'hybrid') {
return 'This package is hybrid. Build with OD-normalized tokens first, then inspect pull-layer source evidence or snippets only when original component behavior, density, or naming would materially improve fidelity.';
}
if (importMode === 'verbatim') {
return 'This package is verbatim-oriented. Preserve source semantics and source naming as much as possible. Before translating component behavior, inspect the relevant pull-layer source evidence or snippets when the runtime tool is available.';
}
return undefined;
}
export interface ComposeInput {
agentId?: string | null | undefined;
includeCodexImagegenOverride?: boolean | undefined;
streamFormat?: string | undefined;
skillBody?: string | undefined;
skillName?: string | undefined;
skillMode?:
| 'prototype'
| 'deck'
| 'template'
| 'design-system'
| 'image'
| 'video'
| 'audio'
| undefined;
skillModes?: Array<'prototype' | 'deck' | 'template' | 'design-system' | 'image' | 'video' | 'audio'> | undefined;
designSystemBody?: string | undefined;
designSystemTitle?: string | undefined;
// Compiled (machine-readable) form of the active brand's design system,
// shipped as sibling files to DESIGN.md when available. Both fields are
// optional; the daemon populates them by default for every brand that
// ships `tokens.css` / `components.html` (today: `default` and
// `kami`). `OD_DESIGN_TOKEN_CHANNEL=0` disables the channel as a kill
// switch. When present they are appended AFTER the DESIGN.md block so
// prose still sets the high-level voice and the structured form
// disambiguates token names + worked component shapes.
//
// - `designSystemUsageMd` — optional USAGE.md router that tells
// agents how to consume this package.
// - `designSystemTokensCss` — verbatim `tokens.css` :root contract
// that the agent pastes into the
// artifact's <style>.
// - `designSystemComponentsManifest` — concise structured summary
// derived from components.html.
// - `designSystemFixtureHtml` — verbatim `components.html`
// fallback when no manifest can
// be derived.
// - `designSystemPullIndex` — lightweight manifest-derived
// list of richer files available
// for later pull-channel work.
designSystemUsageMd?: string | undefined;
designSystemTokensCss?: string | undefined;
designSystemComponentsManifest?: string | undefined;
designSystemFixtureHtml?: string | undefined;
designSystemPullIndex?: string | undefined;
designSystemImportMode?: 'normalized' | 'hybrid' | 'verbatim' | undefined;
// Craft references the active skill opted into via `od.craft.requires`.
// The daemon resolves the slug list to file contents and concatenates
// them with section headers; we inject them between the DESIGN.md and
// the skill body so brand tokens win on conflict but craft rules
// (letter-spacing, accent caps, anti-slop) cover everything below.
craftBody?: string | undefined;
craftSections?: string[] | undefined;
// Markdown built from the user's auto-memory store
// (<dataDir>/memory/*.md). Folded in before the active design system so
// tone/voice/preferences extracted from past chats win over the
// built-in identity charter but still defer to the brand's hard tokens
// and the active skill's workflow. Empty/undefined skips the block.
memoryBody?: string | undefined;
// Per-hook switches for the two-loop memory feature, mirrored from the
// memory config (`profileEnabled` / `rewriteEnabled` / `verifyEnabled`).
// An absent object — or an absent field — is treated as TRUE so callers
// with no memory config wired (and the contracts/BYOK fallback) keep the
// loops on by default. `rewrite` drives the PRE intent-gateway task-brief
// card; `verify` drives the POST self-verify scorecard. `profile` is
// consumed by the memory-body composer; it is accepted here only so the
// same object threads through unchanged.
memoryHooks?: { profile?: boolean; rewrite?: boolean; verify?: boolean } | undefined;
// Project-level metadata captured by the new-project panel. Drives the
// agent's understanding of artifact kind, fidelity, speaker-notes intent
// and animation intent. Missing fields are unresolved facts, not automatic
// clarification triggers.
metadata?: ProjectMetadata | undefined;
// The template the user picked in the From-template tab, when present.
// Snapshot of HTML files that the agent should treat as a starting
// reference rather than a fixed deliverable.
template?: ProjectTemplate | undefined;
// Provider voice choices fetched by the daemon/web before composing the
// prompt. Used for ElevenLabs speech discovery so the agent can render
// a select question-form instead of asking the user to paste raw ids.
audioVoiceOptions?: AudioVoiceOption[] | undefined;
// When voice discovery fails, surface the error reason so the agent
// can tell the user why the dropdown is unavailable instead of
// pretending there were simply no voices.
audioVoiceOptionsError?: string | undefined;
// When present and enabled, the Critique Theater protocol addendum is
// concatenated to the end of the composed prompt. Omitting this field
// (or passing cfg.enabled === false) preserves legacy behavior unchanged.
critique?: CritiqueConfig | undefined;
// Brand name and DESIGN.md body. Required when critique is enabled;
// ignored when critique is disabled or omitted.
critiqueBrand?: { name: string; design_md: string } | undefined;
// Skill identifier. Required when critique is enabled;
// ignored when critique is disabled or omitted.
critiqueSkill?: { id: string } | undefined;
// Optional `## Active plugin` / `## Plugin inputs` block. The daemon's
// plugin module renders this from an AppliedPluginSnapshot; we splice
// it in after the active skill so the plugin description sits next to
// its companion skill body in the prompt. Pass undefined when no
// plugin is bound to the run.
pluginBlock?: string | undefined;
// Plan §3.L2 / spec §23.4 — pre-rendered `## Active stage: <id>`
// blocks (one per pipeline stage active for the run). The daemon's
// pipeline runner builds these from `loadAtomBodies()` +
// `renderActiveStageBlock()` when the OD_BUNDLED_ATOM_PROMPTS env
// flag is set; otherwise this stays undefined and the prompt
// composer's hard-coded constants keep their precedence (back-compat).
activeStageBlocks?: ReadonlyArray<string> | undefined;
// Free-form instructions the user set at the global (user-level)
// settings panel. Injected after personal memory and before the
// project-level instructions.
userInstructions?: string | undefined;
// Free-form instructions the user set on this specific project.
// Injected after user-level instructions and before the design system.
projectInstructions?: string | undefined;
// UI locale selected by the client. User-visible generated form copy
// must follow this locale even when the user's initial prompt is brief.
locale?: string | undefined;
// Per-conversation mode. Design mode keeps the artifact-first agent
// workflow; Plan mode creates an editable source-of-truth document first;
// chat mode keeps the same context/tools but answers like a standard
// multi-turn assistant unless the user explicitly asks to build.
sessionMode?: ChatSessionMode | undefined;
// Run-scoped media policy. Defaults to enabled when omitted so existing
// local OD behavior keeps the same media prompt contract.
mediaExecution?: MediaExecutionPolicy | undefined;
// Run-scoped BYOK media defaults selected in the chat UI.
byokMediaDefaults?: ByokMediaDefaults | undefined;
// Explicit handoff profile. Filesystem runs write project files through
// native tools; text_artifact runs (BYOK/plain) deliver source through
// assistant-text <artifact> blocks.
executionProfile?: ExecutionProfile | undefined;
// Whether the outgoing request text reads as a slide-deck brief (see
// `detectDeckIntentSignal`). Only consulted for the freeform maybe-deck
// branch: `false` skips the ~20K conditional framework injection,
// `true`/`undefined` keep it. Deck-kind projects ignore this — their
// framework is unconditional.
freeformDeckSignal?: boolean | undefined;
// Which always-on doctrine core to compose. `classic` (default) keeps the
// legacy DISCOVERY_AND_PHILOSOPHY + designer-charter stack plus its tail
// overrides. `slim` swaps all of that for the single rewritten charter in
// `core-slim.ts` (every rule stated once, explicit precedence ladder,
// ~6x smaller); the tail overrides it absorbed (filesystem handoff,
// active-DS direction, mid-conversation clarifying questions) are then
// skipped. Daemon callers select it via OD_PROMPT_CORE=slim.
promptCoreVariant?: 'classic' | 'slim' | undefined;
// Whether the visible conversation mentions generating media (see
// `detectMediaIntentSignal`). Only consulted for non-media projects:
// `false` skips the MEDIA_DISPATCH_HINT, `true`/`undefined` keep it.
// Media surfaces always get the full media contract regardless.
mediaHintSignal?: boolean | undefined;
// Whether the visible conversation names a delivery platform (see
// `detectPlatformIntentSignal`). ORed with the metadata-based platform
// gate for PLATFORM_CONTRACTS_BLOCK under slim; absent = metadata only.
platformHintSignal?: boolean | undefined;
}
export function composeSystemPrompt({
agentId,
includeCodexImagegenOverride = true,
skillBody,
skillName,
skillMode,
skillModes,
designSystemBody,
designSystemTitle,
designSystemUsageMd,
designSystemTokensCss,
designSystemComponentsManifest,
designSystemFixtureHtml,
designSystemPullIndex,
designSystemImportMode,
craftBody,
craftSections,
memoryBody,
memoryHooks,
metadata,
template,
audioVoiceOptions,
audioVoiceOptionsError,
critique,
critiqueBrand,
critiqueSkill,
pluginBlock,
activeStageBlocks,
streamFormat,
locale,
sessionMode,
userInstructions,
projectInstructions,
mediaExecution,
byokMediaDefaults,
executionProfile,
freeformDeckSignal,
promptCoreVariant,
mediaHintSignal,
platformHintSignal,
}: ComposeInput): string {
// Slim core collapses the discovery layer + designer charter + their tail
// overrides into one charter document; the classic stack keeps the legacy
// layered composition until the A/B comparison signs off.
const isSlimCore = promptCoreVariant === 'slim';
const isAskModeEarly = sessionMode === 'chat';
// Media surfaces (image / video / audio) must be resolved BEFORE the head
// is built: their generation contract, rather than the design charter's
// HTML workflow, is the sole workflow authority on these runs.
const isMediaSurfaceEarly =
skillMode === 'image' ||
skillMode === 'video' ||
skillMode === 'audio' ||
metadata?.kind === 'image' ||
metadata?.kind === 'video' ||
metadata?.kind === 'audio';
const isSlimCharterHead = isSlimCore && !isAskModeEarly && !isMediaSurfaceEarly;
// Head ordering differs by variant, following prompt-caching prefix rules
// (stable content first — see shared prompt-caching guidance):
// - classic: injection resistance FIRST so no later section can override
// it, then mode overrides, then the layered discovery/charter stack.
// - slim (non-ask): the STATIC charter opens the document (it embeds the
// security section right after Precedence), so every conversation shares
// the same cacheable prefix; conversation-stable overrides (mode,
// locale) follow, project context after that, turn-variable blocks last.
// Slim ask mode opens with the ask override — it IS the charter for the
// turn — with the security section reading as its first subsection, so the
// ask document keeps the same identity-first H1 > H2 hierarchy as design
// mode. Both blocks are static, so the swap is cache-neutral.
// Plain-stream (BYOK/API) slim runs put the API-mode override BEFORE the
// charter: its "every later instruction … is overridden" scope must cover
// the charter's TodoWrite/render instructions, which classic guaranteed by
// always composing the override first. Cache-neutral — plain runs use the
// text_artifact charter variant and form their own prefix family anyway.
const parts: string[] = isSlimCharterHead
? [
...(streamFormat === 'plain' ? [API_MODE_OVERRIDE, '\n\n---\n\n'] : []),
renderSlimCoreCharter(
executionProfile ?? executionProfileFromStreamFormat(streamFormat),
),
'\n\n---\n\n',
]
: isSlimCore && isAskModeEarly
? [
// Ask mode on a plain stream still leads with the API override so
// its "overrides every rule below" scope covers the chat charter,
// matching classic's authority order (API before CHAT).
...(streamFormat === 'plain' ? [API_MODE_OVERRIDE, '\n\n---\n\n'] : []),
CHAT_MODE_OVERRIDE,
'\n\n---\n\n',
PROMPT_INJECTION_RESISTANCE,
'\n\n---\n\n',
]
: isSlimCore
? [
// Slim MEDIA runs (non-ask): no design charter and no Ask charter
// either — CHAT_MODE_OVERRIDE forbids creating media, which would
// contradict the media-generation contract appended below as the
// sole workflow authority. Keep classic's skeleton: API override
// first on plain streams, then injection resistance.
...(streamFormat === 'plain' ? [API_MODE_OVERRIDE, '\n\n---\n\n'] : []),
PROMPT_INJECTION_RESISTANCE,
'\n\n---\n\n',
]
: [PROMPT_INJECTION_RESISTANCE, '\n\n---\n\n'];
// The slim charter's plan step is deliberately generic ("use your runtime's
// plan/todo tool, else a numbered list") so it works on codex / opencode /
// ACP agents that have no such tool. Claude-family runs (streamFormat
// 'claude-stream-json': claude, codebuddy, amp) are the only ones with a
// `TodoWrite` tool the host renders as a live Todos card — name the concrete
// tool + its UI benefit here, for that family only.
if (isSlimCharterHead && streamFormat === 'claude-stream-json') {
parts.push(CLAUDE_PLAN_TOOL_NOTE, '\n\n---\n\n');
}
const activeDesignSystemBody = designSystemBody?.trim();
const activeSkillModes = new Set(
Array.isArray(skillModes)
? skillModes.filter(Boolean)
: skillMode
? [skillMode]
: [],
);
const resolvedExclusiveSurface = resolveExclusiveSurface({ metadata, skillMode, skillModes });
const resolvedExecutionProfile =
executionProfile ?? executionProfileFromStreamFormat(streamFormat);
// API/BYOK mode (streamFormat === 'plain'): mirrors the same fix from
// `@open-design/contracts`'s composer. The daemon hits this path for
// any plain-stream adapter (e.g. DeepSeek), so without pinning the
// override above DISCOVERY_AND_PHILOSOPHY here too, those daemon
// agents still emit the `<todo-list>` / `[读取 X]` pseudo-tool
// markup described in #313. Keep the wording byte-identical to the
// contracts copy so both code paths produce the same observable
// behaviour.
// Turn-variable blocks (gated on per-message signals) are pushed LAST under
// slim — after every conversation/project-stable section — so a signal flip
// mid-conversation only invalidates the cached suffix, not the whole prompt.
const slimTurnVariableParts: string[] = [];
if (streamFormat === 'plain' && !isSlimCore) {
// Slim runs (charter head AND ask head) already composed this first.
parts.push(API_MODE_OVERRIDE);
parts.push('\n\n---\n\n');
}
// Ask mode (`chat`) is the deliberately bare conversation mode: the
// CHAT_MODE_OVERRIDE below IS the whole charter, and every artifact-oriented
// block (the ~3k-token discovery layer, direction library, device frames, the
// full designer charter, deck framework, media contracts, codex imagegen
// override, critique panel, DS visual-direction override) is gated off so the
// turn stays cheap. Memory, custom instructions, the active design system,
// attached skills, plugins, MCP tools, and the clarifying-questions surface
// are still composed in — Ask mode is light, not amnesiac.
const isAskMode = sessionMode === 'chat';
if (sessionMode === 'plan') {
parts.push(PLAN_MODE_OVERRIDE);
parts.push('\n\n---\n\n');
} else if (sessionMode === 'chat' && !isSlimCore) {
// Slim ask already opened the document with this override (see head).
parts.push(CHAT_MODE_OVERRIDE);
parts.push('\n\n---\n\n');
}
// Skip the HTML-artifact discovery layer for media surfaces (image / video /
// audio). DISCOVERY_AND_PHILOSOPHY is ~3 000 tokens of rules about question
// forms, brand extraction, direction pickers, and HTML artifact checklist —
// none of which apply to media generation. Including it forces the agent to
// parse and override all of those rules before it can start, adding tokens
// and LLM inference time. The MEDIA_GENERATION_CONTRACT (pushed below) is
// the sole workflow authority for these surfaces.
if (metadata?.examplePrompt === true) {
parts.push(buildExamplePromptOverride(metadata.examplePromptTitle, metadata.examplePromptBrief));
parts.push('\n\n---\n\n');
} else if (metadata?.skipDiscoveryBrief === true) {
parts.push(SKIP_DISCOVERY_BRIEF_OVERRIDE);
parts.push('\n\n---\n\n');
}
const localePrompt = renderUiLocalePrompt(locale, {
includeQuickBriefSamples: !isSlimCore,
});
if (localePrompt) {
parts.push(localePrompt);
parts.push('\n\n---\n\n');
}
if (!isMediaSurfaceEarly && !isAskMode) {
if (!isSlimCore) {
parts.push(renderDiscoveryAndPhilosophy(resolvedExecutionProfile), '\n\n---\n\n');
}
// Direction library is only useful when the agent must pick a visual
// direction itself. When an active design system is present it is the
// visual direction (see ACTIVE_DESIGN_SYSTEM_VISUAL_DIRECTION_OVERRIDE
// below), so the ~6.7KB direction-card catalogue would just be dead
// weight the model is told to ignore. Gate it on the composer-visible
// active-DS signal (stable for the whole session, so the stable-prompt
// fingerprint stays cacheable).
if (!activeDesignSystemBody) {
// Slim carries only the id+label index and the agent pulls the chosen
// direction's full spec via `od tools directions --id <id>` — but ONLY
// on filesystem runs. text_artifact runs (BYOK/plain adapters) have no
// tools to dereference the index, so they keep the full inline library
// like classic; anything less tells them to bind palettes they cannot
// fetch. Classic keeps the inline full library everywhere.
const canPullDirections = resolvedExecutionProfile !== 'text_artifact';
parts.push(
isSlimCore && canPullDirections
? renderDirectionIndexBlock()