Skip to content

Commit 9fd7e5e

Browse files
ENHANCEMENT: When runtime context failed an allow_regex, deny_regex or the built-in validators you can optionally use the return_message argument for the input to send a message in addition to raising an exception (current behavior, and default)
1 parent 1a1ecf9 commit 9fd7e5e

18 files changed

Lines changed: 474 additions & 58 deletions

README.md

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ sampling:
6464
context:
6565
inputs:
6666
- name: user_message
67-
non_empty: true
67+
non_empty:
68+
return_message: "Please enter a message before continuing."
6869
reject_secrets: true
6970
- name: app_context
7071
max_size: 2000
@@ -100,6 +101,10 @@ const result = await kit.renderPrompt({
100101
},
101102
});
102103

104+
if (result.returnMessage) {
105+
return result.returnMessage;
106+
}
107+
103108
// result.request.body is ready for fetch()
104109
const response = await fetch('https://api.openai.com/v1/chat/completions', {
105110
method: 'POST',
@@ -133,6 +138,7 @@ Supported values for `warnings.contextSize` are `auto`, `off`, `result-only`, `c
133138
- **4 provider adapters** — OpenAI, Anthropic, Gemini, OpenRouter — body-only output
134139
- **Validation** — Zod schema validation, Levenshtein-based "did you mean?" for typos, variable usage checks
135140
- **Context hardening** — structured regexes with flags, `/pattern/i` convenience syntax, and built-in `non_empty` / `reject_secrets` validators
141+
- **Optional short-circuit messages** — validators can return a structured `returnMessage` instead of throwing when configured
136142
- **Context size guardrails** — optional per-input `max_size` metadata with non-blocking render-time warnings
137143
- **Warning controls** — top-level config can suppress or emit context size warnings differently in dev and prod
138144
- **Caching** — LRU cache with mtime-based invalidation
@@ -147,35 +153,40 @@ Each adapter produces a `{ body, provider, model }` object shaped for the target
147153
// OpenAI
148154
import { createPromptOpsKit } from 'promptopskit';
149155
const kit = createPromptOpsKit();
150-
const { request } = await kit.renderPrompt({
156+
let result = await kit.renderPrompt({
151157
path: 'hello',
152158
provider: 'openai',
153159
variables: { name: 'World', app_context: 'Welcome screen' },
154160
});
161+
if (!result.request) throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
162+
const { request } = result;
155163
// request.body → { model, messages, temperature, reasoning_effort, ... }
156164

157165
// Anthropic — system is a top-level field, max_tokens defaults to 4096
158-
const { request } = await kit.renderPrompt({
166+
result = await kit.renderPrompt({
159167
path: 'hello',
160168
provider: 'anthropic',
161169
variables: { name: 'World', app_context: 'Welcome screen' },
162170
});
171+
if (!result.request) throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
163172
// request.body → { model, messages, system, max_tokens, ... }
164173

165174
// Gemini — contents/systemInstruction/generationConfig structure
166-
const { request } = await kit.renderPrompt({
175+
result = await kit.renderPrompt({
167176
path: 'hello',
168177
provider: 'gemini',
169178
variables: { name: 'World', app_context: 'Welcome screen' },
170179
});
180+
if (!result.request) throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
171181
// request.body → { contents, systemInstruction, generationConfig, ... }
172182

173183
// OpenRouter — same shape as OpenAI, different provider label
174-
const { request } = await kit.renderPrompt({
184+
result = await kit.renderPrompt({
175185
path: 'hello',
176186
provider: 'openrouter',
177187
variables: { name: 'World', app_context: 'Welcome screen' },
178188
});
189+
if (!result.request) throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
179190
```
180191

181192
Provider adapters are also available as direct imports:
@@ -217,7 +228,7 @@ On the server, adapters also provide async prompt-aware helpers so you can use t
217228
```typescript
218229
import { openaiAdapter } from 'promptopskit/openai';
219230

220-
const request = await openaiAdapter.renderPrompt(
231+
const result = await openaiAdapter.renderPrompt(
221232
{
222233
path: 'summarizePullRequest',
223234
},
@@ -229,6 +240,12 @@ const request = await openaiAdapter.renderPrompt(
229240
strict: true,
230241
},
231242
);
243+
244+
if (!('body' in result)) {
245+
throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
246+
}
247+
248+
const request = result;
232249
```
233250

234251
If you need a different layout, keep passing `sourceDir` and `compiledDir` explicitly.
@@ -246,7 +263,7 @@ import { createUsageTapClient, runOpenAIWithUsageTap } from 'promptopskit/usaget
246263
const kit = createPromptOpsKit({ sourceDir: './prompts' });
247264
const usageTap = createUsageTapClient({ apiKey: process.env.USAGETAP_API_KEY! });
248265

249-
const { request } = await kit.renderPrompt({
266+
const result = await kit.renderPrompt({
250267
path: 'support/reply',
251268
provider: 'openai',
252269
variables: {
@@ -255,6 +272,12 @@ const { request } = await kit.renderPrompt({
255272
},
256273
});
257274

275+
if (!result.request) {
276+
throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
277+
}
278+
279+
const { request } = result;
280+
258281
const tracked = await runOpenAIWithUsageTap(usageTap, {
259282
begin: {
260283
customerId: 'user_123',
@@ -472,7 +495,7 @@ Creates a `PromptOpsKit` instance.
472495

473496
### `kit.renderPrompt(options)`
474497

475-
Renders a prompt for a specific provider. Returns `{ resolved, request, warnings }`.
498+
Renders a prompt for a specific provider. Returns `{ resolved, request?, returnMessage?, warnings }`.
476499

477500
| Option | Type | Description |
478501
|--------|------|-------------|

SKILL.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Rules:
104104
- Use `allow_regex` for allowlist checks and `deny_regex` for blocklist checks on risky inputs
105105
- Prefer structured regexes like `{ pattern, flags }`; `/pattern/i` strings are also accepted and normalized internally
106106
- Use `non_empty: true` for required user text and `reject_secrets: true` for common secret redaction checks
107+
- When the caller should receive a structured fallback message instead of an exception, use object form with `return_message` on `allow_regex`, `deny_regex`, `non_empty`, or `reject_secrets`
107108
- Escape literal braces with `\{{` and `\}}`
108109
- In strict mode, missing variables throw an error
109110
- In permissive mode, unresolved placeholders are left intact
@@ -121,6 +122,8 @@ context:
121122
If a rendered value exceeds `max_size`, `renderPrompt()` emits a non-blocking `POK030` warning.
122123
At render time, callers can also pass `onContextOverflow` to transform oversized values before warnings/rendering.
123124

125+
If a validator declares `return_message`, `renderPrompt()` returns that message in a structured result and omits the provider request instead of throwing for that validation failure. Invalid regex definitions still fail during `validate` and `compile` as `POK013` prompt-authoring errors.
126+
124127
Malformed `allow_regex` and `deny_regex` values fail during `validate` and `compile`, not just at render time. When regex compilation fails, the error includes the prompt id, variable name, field name, and raw configured value.
125128

126129
Example: this is the minimal valid shape for a prompt that references
@@ -271,7 +274,7 @@ const kit = createPromptOpsKit({
271274
},
272275
});
273276
274-
const { request } = await kit.renderPrompt({
277+
const result = await kit.renderPrompt({
275278
path: 'support/reply',
276279
provider: 'openai',
277280
environment: 'production',
@@ -280,6 +283,16 @@ const { request } = await kit.renderPrompt({
280283
app_context: 'Account settings',
281284
},
282285
});
286+
287+
if (result.returnMessage) {
288+
return result.returnMessage;
289+
}
290+
291+
if (!result.request) {
292+
throw new Error('Prompt rendering did not produce a provider request.');
293+
}
294+
295+
const { request } = result;
283296
```
284297

285298
### Use `adapter.renderPrompt()` when:
@@ -292,7 +305,7 @@ const { request } = await kit.renderPrompt({
292305
import path from 'node:path';
293306
import { openaiAdapter } from 'promptopskit/openai';
294307
295-
const request = await openaiAdapter.renderPrompt(
308+
const result = await openaiAdapter.renderPrompt(
296309
{
297310
path: 'support/reply',
298311
sourceDir: path.join(process.cwd(), 'prompts'),
@@ -307,6 +320,16 @@ const request = await openaiAdapter.renderPrompt(
307320
strict: true,
308321
},
309322
);
323+
324+
if (result.returnMessage) {
325+
return result.returnMessage;
326+
}
327+
328+
if (!('body' in result)) {
329+
throw new Error('Prompt rendering did not produce a provider request.');
330+
}
331+
332+
const request = result;
310333
```
311334

312335
### Use `adapter.render()` when:
@@ -438,7 +461,7 @@ Hello {{ name }}
438461
| Command | Description |
439462
|---------|-------------|
440463
| `promptopskit init [dir]` | Scaffold a prompts directory with starter files (including `defaults.md`) |
441-
| `promptopskit validate <dir>` | Validate all prompt files in a directory |
464+
| `promptopskit validate [sourceDir] [options]` | Validate all prompt files in a directory, defaulting to `./prompts` |
442465
| `promptopskit compile [src] [out]` | Compile `.md` prompts to JSON or ESM artifacts |
443466
| `promptopskit render <file>` | Render a prompt preview |
444467
| `promptopskit inspect <file>` | Print the normalized prompt asset |

docs/api-reference.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const kit = createPromptOpsKit({
4242

4343
## `kit.renderPrompt(options)`
4444

45-
Renders a prompt for a specific provider. Returns `{ resolved, request, warnings }`.
45+
Renders a prompt for a specific provider. Returns `{ resolved, request?, returnMessage?, warnings }`.
4646

4747
```typescript
4848
const result = await kit.renderPrompt({
@@ -79,13 +79,16 @@ Either `path` or `source` must be provided.
7979
```typescript
8080
interface RenderResult {
8181
resolved: ResolvedPromptAsset; // Fully resolved asset
82-
request: ProviderRequest; // { body, provider, model }
82+
request?: ProviderRequest; // { body, provider, model } when rendering continues
83+
returnMessage?: string; // Short-circuit message from context validation when configured
8384
warnings: string[]; // Non-fatal provider and render-time warnings
8485
}
8586
```
8687

8788
`warnings` may include provider adapter warnings and render-time `POK030` context size warnings when configured to be included in results.
8889

90+
If a context validator fails and that validator declares `return_message`, `renderPrompt()` returns `returnMessage` and omits `request` instead of throwing.
91+
8992
## `kit.loadPrompt(path)`
9093

9194
Load a prompt asset from compiled or source (based on mode). Returns a `PromptAsset`.

docs/getting-started.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ const result = await kit.renderPrompt({
7979
},
8080
});
8181

82+
if (!result.request) {
83+
return result.returnMessage;
84+
}
85+
8286
// result.request.body is ready for fetch()
8387
const response = await fetch('https://api.openai.com/v1/chat/completions', {
8488
method: 'POST',

docs/inline-source.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ Hello {{ name }}!`,
2525
provider: 'openai',
2626
variables: { name: 'World' },
2727
});
28+
29+
if (!result.request) {
30+
throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
31+
}
2832
```
2933

3034
## With overrides
@@ -51,6 +55,10 @@ Hello {{ name }}!`,
5155
variables: { name: 'World' },
5256
});
5357

58+
if (!result.request) {
59+
throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
60+
}
61+
5462
// result.request.body.model === 'gpt-5.4-mini'
5563
```
5664

@@ -73,6 +81,10 @@ schema_version: 1
7381
provider: 'openai',
7482
variables: { question: 'What is 2+2?' },
7583
});
84+
85+
if (!result.request) {
86+
throw new Error(result.returnMessage ?? 'Prompt rendering failed.');
87+
}
7688
```
7789

7890
## Limitations

docs/prompt-format.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,10 @@ Each entry can be either a string variable name or an object with:
169169
- `name` — the template variable name
170170
- `max_size` — optional UTF-8 byte limit for the injected value
171171
- `trim` — optional trim-to-budget (`true`/`end` keeps first bytes, `start` keeps trailing bytes) applied when `max_size` is set
172-
- `allow_regex` — optional allowlist regex; accepts `"pattern"`, `/pattern/i`, or `{ pattern, flags }` and throws `POK031` on mismatch
173-
- `deny_regex` — optional blocklist regex; accepts `"pattern"`, `/pattern/i`, or `{ pattern, flags }` and throws `POK032` on match
174-
- `non_empty` — optional boolean validator; throws `POK033` when the final value is blank or whitespace-only
175-
- `reject_secrets` — optional boolean validator; throws `POK034` when the value matches the built-in secret detector
172+
- `allow_regex` — optional allowlist regex; accepts `"pattern"`, `/pattern/i`, or `{ pattern, flags, return_message? }` and throws `POK031` on mismatch unless `return_message` is configured
173+
- `deny_regex` — optional blocklist regex; accepts `"pattern"`, `/pattern/i`, or `{ pattern, flags, return_message? }` and throws `POK032` on match unless `return_message` is configured
174+
- `non_empty` — optional boolean or object validator; use `true` to throw `POK033`, or `{ return_message }` to short-circuit rendering with a structured message
175+
- `reject_secrets` — optional boolean or object validator; use `true` to throw `POK034`, or `{ return_message }` to short-circuit rendering with a structured message
176176

177177
The validator warns about:
178178
- Variables used in templates but not declared in `context.inputs`
@@ -193,8 +193,10 @@ context:
193193
allow_regex:
194194
pattern: "^user_[a-z0-9]+$"
195195
flags: "i"
196+
return_message: "User IDs must use the user_123 format."
196197
- name: pull_request_body
197-
non_empty: true
198+
non_empty:
199+
return_message: "Pull request content is required."
198200
reject_secrets: true
199201
deny_regex: "/(ignore previous instructions|system:)/i"
200202
```

0 commit comments

Comments
 (0)