feat(core): add fallback support and falsy value normalization to PromptSection

This commit is contained in:
Michael Bleigh
2026-03-21 16:02:47 -07:00
parent 20aba13eb9
commit 8ed910051d
2 changed files with 85 additions and 6 deletions
@@ -284,6 +284,60 @@ const tests: TestCase[] = [
context: {},
expect: '# Lines List\n\nLine 1\nLine 2\nLine 3',
},
{
desc: 'renders fallback when content is falsy (boolean)',
content: {
heading: 'Fallback Boolean',
content: false,
fallback: 'This is the fallback',
},
context: {},
expect: '# Fallback Boolean\n\nThis is the fallback',
},
{
desc: 'renders fallback when content is falsy (empty string via short-circuit)',
content: {
heading: 'Fallback Empty String',
content: String('') && 'something',
fallback: 'Fallback for empty string',
},
context: {},
expect: '# Fallback Empty String\n\nFallback for empty string',
},
{
desc: 'renders fallback when content is an array that filters down to empty',
content: {
heading: 'Fallback Empty Array',
content: [false, null, '', undefined],
fallback: 'Fallback for empty array',
},
context: {},
expect: '# Fallback Empty Array\n\nFallback for empty array',
},
{
desc: 'renders normal content and ignores fallback when content is present',
content: {
heading: 'No Fallback',
content: 'Primary content',
fallback: 'Should not see this',
},
context: {},
expect: '# No Fallback\n\nPrimary content',
},
{
desc: 'does not render section if both content and fallback are falsy',
content: [
'Visible start',
{
heading: 'Double Falsy',
content: '',
fallback: null,
},
'Visible end',
],
context: {},
expect: 'Visible start\n\nVisible end',
},
];
describe('renderPrompt', () => {
+31 -6
View File
@@ -34,6 +34,8 @@ export type PromptSection<C, Sync extends boolean = false> = {
/** Condition that must evaluate to true for the section to be rendered. */
condition?: boolean | ((ctx: C) => MaybePromise<boolean, Sync>);
content: PromptContent<C, Sync>;
/** Alternate content to render if the primary content resolves to a falsy value. */
fallback?: PromptContent<C, Sync>;
};
// The core recursive type.
@@ -189,7 +191,8 @@ export async function renderPrompt<C = SystemPromptOptions>({
return null;
}
if (typeof c === 'string' || typeof c === 'number') {
return String(c);
const val = String(c);
return val === '' ? null : val;
}
if (Array.isArray(c)) {
const resolved = await Promise.all(c.map((item) => resolveToBasic(item)));
@@ -212,7 +215,18 @@ export async function renderPrompt<C = SystemPromptOptions>({
: section.condition;
if (!shouldRender) return null;
}
const resolvedInner = await resolveToBasic(section.content);
let resolvedInner = await resolveToBasic(section.content);
if (
resolvedInner === null ||
resolvedInner === '' ||
(Array.isArray(resolvedInner) && resolvedInner.length === 0)
) {
if (section.fallback !== undefined) {
resolvedInner = await resolveToBasic(section.fallback);
}
}
if (
resolvedInner === null ||
resolvedInner === '' ||
@@ -294,7 +308,8 @@ export function renderPromptSync<C = SystemPromptOptions>({
return null;
}
if (typeof c === 'string' || typeof c === 'number') {
return String(c);
const val = String(c);
return val === '' ? null : val;
}
if (Array.isArray(c)) {
const resolved = c.map((item) => resolveToBasicSync(item));
@@ -324,7 +339,18 @@ export function renderPromptSync<C = SystemPromptOptions>({
}
if (!shouldRender) return null;
}
const resolvedInner = resolveToBasicSync(section.content);
let resolvedInner = resolveToBasicSync(section.content);
if (
resolvedInner === null ||
resolvedInner === '' ||
(Array.isArray(resolvedInner) && resolvedInner.length === 0)
) {
if (section.fallback !== undefined) {
resolvedInner = resolveToBasicSync(section.fallback);
}
}
if (
resolvedInner === null ||
resolvedInner === '' ||
@@ -366,6 +392,5 @@ export function renderPromptSync<C = SystemPromptOptions>({
export function prompt<C = SystemPromptOptions, Sync extends boolean = false>(
...content: Array<PromptContent<C, Sync>>
): PromptContent<C, Sync> {
return (content.length === 1 ? content[0] : content);
return content.length === 1 ? content[0] : content;
}