@x12i/ai-gateway 10.4.3 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,19 +5,38 @@
5
5
  */
6
6
  import { parseTemplate } from './template-parser.js';
7
7
  import { mergeGatewayAndRequestTemplateRenderOptions } from './template-render-merge.js';
8
- // Type guard
9
- // AIRequest is distinguished by having primaryObjectType or objectTypes
10
- // ChatRequest does not have these fields
11
- function isAIRequest(request) {
12
- return 'primaryObjectType' in request || ('objectTypes' in request && Array.isArray(request.objectTypes));
8
+ function hasNonEmptyPrompt(prompt) {
9
+ return typeof prompt === 'string' && prompt.trim() !== '';
10
+ }
11
+ function getWorkingMemoryInput(workingMemory) {
12
+ if (workingMemory == null || typeof workingMemory !== 'object')
13
+ return undefined;
14
+ return workingMemory.input;
15
+ }
16
+ function hasResolvableInput(workingMemory) {
17
+ const input = getWorkingMemoryInput(workingMemory);
18
+ return input !== undefined && input !== null;
19
+ }
20
+ /**
21
+ * Serializes workingMemory.input when no prompt template is provided.
22
+ */
23
+ export function formatInputFallback(input) {
24
+ if (input === undefined || input === null) {
25
+ return '';
26
+ }
27
+ if (typeof input === 'string') {
28
+ return input;
29
+ }
30
+ if (typeof input === 'object') {
31
+ return JSON.stringify(input, null, 2);
32
+ }
33
+ return String(input);
13
34
  }
14
35
  /**
15
- * Builds user message (prompt + input)
36
+ * Builds user message from prompt template or workingMemory.input fallback.
16
37
  */
17
38
  async function buildUserMessage(request, config, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions) {
18
39
  const { logger } = config;
19
- const parts = [];
20
- // Validate that input field is not provided (removed - use workingMemory.input instead)
21
40
  if ('input' in request && request.input !== undefined) {
22
41
  const err = new Error(`The 'input' field has been removed. Use workingMemory.input instead for template rendering. Prompt templates should contain {{input}} which will be resolved from workingMemory.input.`);
23
42
  err.code = 'INPUT_FIELD_DEPRECATED';
@@ -32,213 +51,127 @@ async function buildUserMessage(request, config, shortTermMemory, experienceMemo
32
51
  errorCode: 'INPUT_FIELD_DEPRECATED',
33
52
  hasPrompt: !!request.prompt,
34
53
  hasWorkingMemory: !!request.workingMemory,
35
- hasInputInWorkingMemory: !!request.workingMemory?.input
54
+ hasInputInWorkingMemory: hasResolvableInput(request.workingMemory)
36
55
  });
37
56
  throw err;
38
57
  }
39
- // Determine if we have user content to process (prompt is required for user message)
40
- const hasUserContent = isAIRequest(request) && request.prompt;
41
- // Input prefix is no longer used - prompt templates handle all formatting
42
- // If no prompt is provided, that's an error (prompt is required for user message)
43
- if (isAIRequest(request) && !request.prompt) {
44
- const err = new Error(`Prompt is required for AI requests. Provide prompt template text. The template should contain variables such as {{input}} resolved from workingMemory.input.`);
45
- err.code = 'PROMPT_REQUIRED';
46
- err.details = {
47
- missingField: 'prompt',
48
- hasWorkingMemory: !!request.workingMemory,
49
- hasInputInWorkingMemory: !!request.workingMemory?.input
50
- };
51
- logger.error('Prompt is required for AI requests', {
58
+ if (hasNonEmptyPrompt(request.prompt)) {
59
+ if (typeof request.prompt !== 'string') {
60
+ throw new Error(`Prompt must be a string template, but received: ${typeof request.prompt}`);
61
+ }
62
+ logger.info('Parsing prompt template text', {
52
63
  jobId: request.identity.jobId,
53
64
  agentId: request.agentId,
54
- errorCode: 'PROMPT_REQUIRED'
65
+ promptLength: request.prompt.length,
66
+ promptPreview: request.prompt.substring(0, 200),
67
+ hasWorkingMemory: !!request.workingMemory
55
68
  });
56
- throw err;
57
- }
58
- // Add prompt if provided (always template text; parsed with memory context)
59
- if (request.prompt) {
60
- if (typeof request.prompt === 'string') {
61
- logger.info('Parsing prompt template text', {
62
- jobId: request.identity.jobId,
63
- agentId: request.agentId,
64
- promptLength: request.prompt.length,
65
- promptPreview: request.prompt.substring(0, 200),
66
- hasWorkingMemory: !!request.workingMemory
67
- });
68
- try {
69
- if (!request.workingMemory) {
70
- const err = new Error(`workingMemory is required for prompt template rendering but was not provided.`);
71
- err.code = 'WORKING_MEMORY_REQUIRED';
72
- err.details = { requiresVariables: true };
73
- logger.error('workingMemory required for prompt template rendering', {
74
- jobId: request.identity.jobId,
75
- agentId: request.agentId,
76
- errorCode: 'WORKING_MEMORY_REQUIRED'
77
- });
78
- throw err;
79
- }
80
- const parsedPrompt = await parseTemplate(request.prompt, request.workingMemory, undefined, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
81
- if (!parsedPrompt || parsedPrompt.trim() === '') {
82
- const workingMemoryObj = request.workingMemory;
83
- const err = new Error(`Prompt template rendered to empty string. This may indicate missing template variables or empty template content.`);
84
- err.code = 'PROMPT_RENDERED_EMPTY';
85
- err.details = {
86
- promptLength: request.prompt.length,
87
- renderedLength: 0,
88
- hasWorkingMemory: !!request.workingMemory,
89
- workingMemoryKeys: workingMemoryObj ? Object.keys(workingMemoryObj) : []
90
- };
91
- logger.error('Prompt template rendered to empty string', {
92
- jobId: request.identity.jobId,
93
- agentId: request.agentId,
94
- errorCode: 'PROMPT_RENDERED_EMPTY',
95
- hasWorkingMemory: !!request.workingMemory,
96
- workingMemoryKeys: workingMemoryObj ? Object.keys(workingMemoryObj) : []
97
- });
98
- throw err;
99
- }
100
- logger.info('Prompt text parsed successfully', {
69
+ try {
70
+ if (!request.workingMemory) {
71
+ const err = new Error(`workingMemory is required for prompt template rendering but was not provided.`);
72
+ err.code = 'WORKING_MEMORY_REQUIRED';
73
+ err.details = { requiresVariables: true };
74
+ logger.error('workingMemory required for prompt template rendering', {
101
75
  jobId: request.identity.jobId,
102
76
  agentId: request.agentId,
103
- originalLength: request.prompt.length,
104
- parsedLength: parsedPrompt.length,
105
- parsedPreview: parsedPrompt.substring(0, 200)
77
+ errorCode: 'WORKING_MEMORY_REQUIRED'
106
78
  });
107
- parts.push(parsedPrompt);
79
+ throw err;
108
80
  }
109
- catch (error) {
110
- const err = error instanceof Error ? error : new Error(String(error));
111
- let errorCode = 'PROMPT_TEMPLATE_ERROR';
112
- let errorMessage = `Failed to render prompt template: ${err.message}`;
113
- if (err.message.includes('not found') || err.message.includes('does not exist')) {
114
- errorCode = 'PROMPT_NOT_FOUND';
115
- errorMessage = err.message;
116
- }
117
- else if (err.message.includes('rendered to empty')) {
118
- errorCode = 'PROMPT_RENDERED_EMPTY';
119
- errorMessage = err.message;
120
- }
121
- else if (err.name === 'TemplateResolutionError') {
122
- errorCode = 'TEMPLATE_RESOLUTION_ERROR';
123
- errorMessage = err.message;
124
- }
125
- else if (err.message.includes('Template variable') || err.message.includes('missing')) {
126
- errorCode = 'TEMPLATE_VARIABLE_MISSING';
127
- errorMessage = err.message;
128
- }
129
- logger.error('Failed to render prompt template', {
81
+ const parsedPrompt = await parseTemplate(request.prompt, request.workingMemory, undefined, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
82
+ if (!parsedPrompt || parsedPrompt.trim() === '') {
83
+ const workingMemoryObj = request.workingMemory;
84
+ const err = new Error(`Prompt template rendered to empty string. This may indicate missing template variables or empty template content.`);
85
+ err.code = 'PROMPT_RENDERED_EMPTY';
86
+ err.details = {
87
+ promptLength: request.prompt.length,
88
+ renderedLength: 0,
89
+ hasWorkingMemory: !!request.workingMemory,
90
+ workingMemoryKeys: workingMemoryObj ? Object.keys(workingMemoryObj) : []
91
+ };
92
+ logger.error('Prompt template rendered to empty string', {
130
93
  jobId: request.identity.jobId,
131
94
  agentId: request.agentId,
132
- errorCode,
133
- error: err.message,
134
- errorName: err.name,
135
- errorStack: err.stack
95
+ errorCode: 'PROMPT_RENDERED_EMPTY',
96
+ hasWorkingMemory: !!request.workingMemory,
97
+ workingMemoryKeys: workingMemoryObj ? Object.keys(workingMemoryObj) : []
136
98
  });
137
- const structuredError = new Error(errorMessage);
138
- structuredError.code = errorCode;
139
- structuredError.details = {
140
- type: 'prompt',
141
- originalError: err.message
142
- };
143
- throw structuredError;
99
+ throw err;
144
100
  }
101
+ logger.info('Prompt text parsed successfully', {
102
+ jobId: request.identity.jobId,
103
+ agentId: request.agentId,
104
+ originalLength: request.prompt.length,
105
+ parsedLength: parsedPrompt.length,
106
+ parsedPreview: parsedPrompt.substring(0, 200)
107
+ });
108
+ return parsedPrompt;
145
109
  }
146
- else {
147
- const err = new Error(`Prompt must be a string template, but received: ${typeof request.prompt}`);
148
- logger.error('Prompt provided as non-string - not supported', {
110
+ catch (error) {
111
+ const err = error instanceof Error ? error : new Error(String(error));
112
+ let errorCode = 'PROMPT_TEMPLATE_ERROR';
113
+ let errorMessage = `Failed to render prompt template: ${err.message}`;
114
+ if (err.message.includes('not found') || err.message.includes('does not exist')) {
115
+ errorCode = 'PROMPT_NOT_FOUND';
116
+ errorMessage = err.message;
117
+ }
118
+ else if (err.message.includes('rendered to empty')) {
119
+ errorCode = 'PROMPT_RENDERED_EMPTY';
120
+ errorMessage = err.message;
121
+ }
122
+ else if (err.name === 'TemplateResolutionError') {
123
+ errorCode = 'TEMPLATE_RESOLUTION_ERROR';
124
+ errorMessage = err.message;
125
+ }
126
+ else if (err.message.includes('Template variable') || err.message.includes('missing')) {
127
+ errorCode = 'TEMPLATE_VARIABLE_MISSING';
128
+ errorMessage = err.message;
129
+ }
130
+ logger.error('Failed to render prompt template', {
149
131
  jobId: request.identity.jobId,
150
132
  agentId: request.agentId,
151
- promptType: typeof request.prompt
133
+ errorCode,
134
+ error: err.message,
135
+ errorName: err.name,
136
+ errorStack: err.stack
152
137
  });
153
- throw err;
138
+ const structuredError = new Error(errorMessage);
139
+ structuredError.code = errorCode;
140
+ structuredError.details = {
141
+ type: 'prompt',
142
+ originalError: err.message
143
+ };
144
+ throw structuredError;
154
145
  }
155
146
  }
156
- // Input field has been removed - all input must come from workingMemory.input
157
- // Prompt templates should contain {{input}} which will be resolved from workingMemory.input
158
- // No need to add input separately - it's already in the rendered prompt template
159
- const userMessage = parts.join('\n\n');
160
- // If prompt was provided, we MUST have a non-empty user message
161
- if (request.prompt && (!userMessage || userMessage.trim() === '')) {
162
- const err = new Error(`Prompt template was provided but resulted in empty user message. The template may have rendered to empty or failed to resolve.`);
163
- err.code = 'PROMPT_NO_USER_MESSAGE';
164
- err.details = {
165
- promptPreview: typeof request.prompt === 'string' ? request.prompt.substring(0, 200) : request.prompt,
166
- hasWorkingMemory: !!request.workingMemory,
167
- workingMemoryKeys: request.workingMemory ? Object.keys(request.workingMemory) : [],
168
- partsLength: parts.length
169
- };
170
- logger.error('Prompt provided but resulted in empty user message', {
147
+ if (hasResolvableInput(request.workingMemory)) {
148
+ const fallback = formatInputFallback(getWorkingMemoryInput(request.workingMemory));
149
+ if (!fallback.trim()) {
150
+ const err = new Error(`workingMemory.input is present but empty. Provide a prompt template or non-empty workingMemory.input.`);
151
+ err.code = 'USER_CONTENT_REQUIRED';
152
+ throw err;
153
+ }
154
+ logger.info('Using workingMemory.input fallback for user message', {
171
155
  jobId: request.identity.jobId,
172
156
  agentId: request.agentId,
173
- prompt: request.prompt,
174
- errorCode: 'PROMPT_NO_USER_MESSAGE',
175
- partsLength: parts.length,
176
- parts: parts.map(p => ({ length: p.length, preview: p.substring(0, 50) }))
157
+ fallbackLength: fallback.length,
158
+ fallbackPreview: fallback.substring(0, 200)
177
159
  });
178
- throw err;
179
- }
180
- return userMessage;
181
- }
182
- /**
183
- * Checks if instructions already meet the required flex-md compliance level
184
- * Uses flex-md SDK to validate compliance
185
- *
186
- * @param instructionsText - Instructions to check
187
- * @param complianceLevel - Required compliance level (L0, L1, L2, L3)
188
- * @returns true if instructions meet the compliance level
189
- */
190
- async function hasFlexMdContract(instructionsText, complianceLevel = 'L0') {
191
- if (!instructionsText || instructionsText.trim() === '') {
192
- return false;
193
- }
194
- try {
195
- // Try to use flex-md SDK to validate compliance
196
- const { loadFlexMd } = await import('./flex-md-loader.js');
197
- const flexMd = await loadFlexMd();
198
- // Check if validateMarkdownAgainstOfs is available
199
- if (flexMd.validateMarkdownAgainstOfs && typeof flexMd.validateMarkdownAgainstOfs === 'function') {
200
- try {
201
- // Try to validate - this might require a spec, so we catch errors
202
- const result = flexMd.validateMarkdownAgainstOfs(instructionsText, { strictness: complianceLevel });
203
- // If validation passes, instructions meet compliance
204
- if (result && result.valid !== false) {
205
- return true;
206
- }
207
- }
208
- catch (e) {
209
- // Validation function might need more parameters, fall through to pattern matching
210
- }
211
- }
212
- }
213
- catch (error) {
214
- // flex-md SDK not available or error - fall through to pattern matching
215
- }
216
- // Fallback: Pattern-based checking
217
- const text = instructionsText.toLowerCase();
218
- // Check for key flex-md enforcement phrases
219
- const flexMdIndicators = [
220
- 'markdown',
221
- 'fenced block',
222
- '```markdown',
223
- '```json',
224
- 'reply in markdown',
225
- 'return your entire answer'
226
- ];
227
- // Check for enforcement language based on compliance level
228
- const enforcementIndicators = [];
229
- if (complianceLevel === 'L2' || complianceLevel === 'L3') {
230
- // L2/L3 require fenced block container
231
- enforcementIndicators.push('fenced block', '```markdown', 'single ```markdown', 'entire answer inside', 'nothing else');
232
- }
233
- if (complianceLevel === 'L1' || complianceLevel === 'L2' || complianceLevel === 'L3') {
234
- // L1+ requires section headings
235
- enforcementIndicators.push('section', 'heading', 'include these');
160
+ return fallback;
236
161
  }
237
- // Must have at least one flex-md indicator AND appropriate enforcement indicators
238
- const hasFlexMd = flexMdIndicators.some(indicator => text.includes(indicator));
239
- const hasEnforcement = enforcementIndicators.length === 0 ||
240
- enforcementIndicators.some(indicator => text.includes(indicator));
241
- return hasFlexMd && hasEnforcement;
162
+ const err = new Error(`User message requires prompt template text or workingMemory.input. Provide prompt or workingMemory.input.`);
163
+ err.code = 'USER_CONTENT_REQUIRED';
164
+ err.details = {
165
+ hasPrompt: hasNonEmptyPrompt(request.prompt),
166
+ hasWorkingMemory: !!request.workingMemory,
167
+ hasInputInWorkingMemory: false
168
+ };
169
+ logger.error('No prompt or workingMemory.input for user message', {
170
+ jobId: request.identity.jobId,
171
+ agentId: request.agentId,
172
+ errorCode: 'USER_CONTENT_REQUIRED'
173
+ });
174
+ throw err;
242
175
  }
243
176
  /**
244
177
  * Main function to build messages
@@ -247,13 +180,11 @@ export async function buildMessages(request, config, options = {}) {
247
180
  const { parsedSnapshot } = options;
248
181
  const { logger } = config;
249
182
  const messages = [];
250
- // Step 1: Instructions as template text (parsed with full memory context)
251
- let instructionsText = '';
252
- // Extract memory context from options
253
183
  const shortTermMemory = options.shortTermMemory;
254
184
  const experienceMemory = options.experienceMemory;
255
185
  const knowledgeMemory = options.knowledgeMemory;
256
186
  const templateRenderOptions = mergeGatewayAndRequestTemplateRenderOptions(config.templateRendering, request);
187
+ let instructionsText = '';
257
188
  if (request.instructions) {
258
189
  if (typeof request.instructions === 'string') {
259
190
  logger.info('Using instructions as template text', {
@@ -268,16 +199,9 @@ export async function buildMessages(request, config, options = {}) {
268
199
  instructionsText = JSON.stringify(request.instructions);
269
200
  }
270
201
  }
271
- // NO SYSTEM CONTEXT FALLBACK - removed default instruction fallback
272
- // Instructions must be provided explicitly - no defaults allowed
273
- // If instructionsText is empty here, it's a bad request
274
- // Step 2: Parse instructions template with full memory context
275
- // Rendrix handles token resolution, so we just parse directly
276
202
  if (instructionsText) {
277
- instructionsText = await parseTemplate(instructionsText, request.workingMemory, undefined, // taskConfig removed - no longer used
278
- shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
203
+ instructionsText = await parseTemplate(instructionsText, request.workingMemory, undefined, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
279
204
  }
280
- // Instructions must be provided explicitly — no packaged block injection
281
205
  if (!instructionsText || instructionsText.trim() === '') {
282
206
  const errorMessage = 'No instructions available - cannot proceed without clear instructions. This is a bad request.';
283
207
  logger.error(errorMessage, {
@@ -292,11 +216,9 @@ export async function buildMessages(request, config, options = {}) {
292
216
  role: 'system',
293
217
  content: instructionsText
294
218
  });
295
- // Store resolved/parsed content for activity (parsed = content after resolution + Rendrix, not the key)
296
219
  if (parsedSnapshot) {
297
220
  parsedSnapshot.instructions = instructionsText;
298
221
  }
299
- // Log the final system message for verification
300
222
  logger.info('Final system message constructed', {
301
223
  jobId: request.identity.jobId,
302
224
  agentId: request.agentId,
@@ -305,65 +227,34 @@ export async function buildMessages(request, config, options = {}) {
305
227
  hasFencedBlock: instructionsText.includes('```markdown'),
306
228
  hasJson: instructionsText.toLowerCase().includes('json'),
307
229
  messagePreview: instructionsText.substring(0, 500),
308
- fullMessage: instructionsText // Log full message for debugging
230
+ fullMessage: instructionsText
309
231
  });
310
- // Step 7: Add context (if provided) with full memory context
311
- if (request.context) {
312
- const contextText = typeof request.context === 'string' ? request.context : JSON.stringify(request.context);
313
- const parsedContext = await parseTemplate(contextText, request.workingMemory, undefined, // taskConfig removed - no longer used
314
- shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
315
- if (parsedContext && parsedContext.trim() !== '') {
316
- messages.push({
317
- role: 'assistant',
318
- content: parsedContext
319
- });
320
- }
321
- }
322
- // Step 8: Build user message with full memory context
323
232
  const userMessage = await buildUserMessage(request, config, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions);
324
- if (userMessage && userMessage.trim() !== '') {
325
- messages.push({
326
- role: 'user',
327
- content: userMessage
328
- });
329
- // Store resolved/parsed prompt for activity (parsed = content after resolution + Rendrix, not the key)
330
- if (parsedSnapshot) {
331
- parsedSnapshot.prompt = userMessage;
332
- }
333
- // Log the user message for verification
334
- logger.info('Final user message constructed', {
233
+ if (!userMessage || userMessage.trim() === '') {
234
+ const err = new Error(`User message is required but was empty after prompt render or input fallback.`);
235
+ err.code = 'USER_CONTENT_REQUIRED';
236
+ logger.error('User message empty after build', {
335
237
  jobId: request.identity.jobId,
336
238
  agentId: request.agentId,
337
- messageLength: userMessage.length,
338
- messagePreview: userMessage.substring(0, 200),
339
- fullMessage: userMessage // Log full message for debugging
239
+ errorCode: 'USER_CONTENT_REQUIRED',
240
+ hasPrompt: hasNonEmptyPrompt(request.prompt)
340
241
  });
242
+ throw err;
341
243
  }
342
- else {
343
- // If prompt was provided, we MUST have a user message - this is an error
344
- if (request.prompt) {
345
- const err = new Error(`Prompt template was provided but no user message was created. The template may have rendered to empty or failed to resolve.`);
346
- err.code = 'PROMPT_NO_USER_MESSAGE';
347
- err.details = {
348
- promptPreview: typeof request.prompt === 'string' ? request.prompt.substring(0, 200) : request.prompt,
349
- hasWorkingMemory: !!request.workingMemory
350
- };
351
- logger.error('Prompt provided but no user message created', {
352
- jobId: request.identity.jobId,
353
- agentId: request.agentId,
354
- prompt: request.prompt,
355
- errorCode: 'PROMPT_NO_USER_MESSAGE'
356
- });
357
- throw err;
358
- }
359
- // If no prompt was provided, it's just a warning (input-only requests are valid)
360
- logger.warn('No user message to add', {
361
- jobId: request.identity.jobId,
362
- agentId: request.agentId,
363
- hasPrompt: !!request.prompt
364
- });
244
+ messages.push({
245
+ role: 'user',
246
+ content: userMessage
247
+ });
248
+ if (parsedSnapshot) {
249
+ parsedSnapshot.prompt = userMessage;
365
250
  }
366
- // Log complete message structure
251
+ logger.info('Final user message constructed', {
252
+ jobId: request.identity.jobId,
253
+ agentId: request.agentId,
254
+ messageLength: userMessage.length,
255
+ messagePreview: userMessage.substring(0, 200),
256
+ fullMessage: userMessage
257
+ });
367
258
  logger.info('Complete message structure', {
368
259
  jobId: request.identity.jobId,
369
260
  agentId: request.agentId,
@@ -28,6 +28,10 @@ export interface BuiltMessages {
28
28
  hasObjectTypes?: boolean;
29
29
  };
30
30
  }
31
+ /**
32
+ * Serializes workingMemory.input when no prompt template is provided.
33
+ */
34
+ export declare function formatInputFallback(input: unknown): string;
31
35
  /**
32
36
  * Main function to build messages
33
37
  */
@@ -195,7 +195,10 @@ export function validateAIRequest(request) {
195
195
  errors.push('instructions is required');
196
196
  }
197
197
  if (!request.prompt) {
198
- errors.push('Prompt is required (input field has been removed - use workingMemory.input instead)');
198
+ const wmInput = request.workingMemory?.input;
199
+ if (wmInput === undefined || wmInput === null) {
200
+ errors.push('prompt or workingMemory.input is required for the user turn');
201
+ }
199
202
  }
200
203
  // Validate config
201
204
  if (!request.config) {
@@ -581,7 +584,7 @@ export function createValidationTestCases() {
581
584
  expectedErrors: ['instructions is required']
582
585
  },
583
586
  {
584
- name: 'Missing prompt',
587
+ name: 'Missing prompt and workingMemory.input',
585
588
  request: {
586
589
  aiRequestId: 'ai-no-prompt',
587
590
  agentId: 'agent-1',
@@ -589,11 +592,11 @@ export function createValidationTestCases() {
589
592
  actionRef: 'validation/x',
590
593
  identity: sampleIdentity('test', 'agent-1'),
591
594
  instructions: 'Test',
592
- workingMemory: { input: 'Test' },
595
+ workingMemory: {},
593
596
  config: { model: 'gpt-4o', provider: 'openai' }
594
597
  },
595
598
  shouldFail: true,
596
- expectedErrors: ['Prompt is required (input field has been removed - use workingMemory.input instead)']
599
+ expectedErrors: ['prompt or workingMemory.input is required for the user turn']
597
600
  },
598
601
  {
599
602
  name: 'Missing actionRef',
@@ -359,6 +359,21 @@ export type GatewayInvokeRejectionMetadata = {
359
359
  * Sourced from {@link FallbackExhaustedError.attempts} on the router error chain.
360
360
  */
361
361
  fallbackAttempts?: GatewayFallbackAttempt[];
362
+ /**
363
+ * Structured per-attempt diagnostics (`@x12i/ai-providers-router` ≥ 4.10).
364
+ * Same attempts as {@link fallbackAttempts} when enriched; prefer this for UIs.
365
+ */
366
+ providerAttempts?: Array<{
367
+ provider: string;
368
+ modelId?: string;
369
+ httpStatus?: number;
370
+ message: string;
371
+ code?: string;
372
+ }>;
373
+ /** Dominant stable failure sub-code when fallback chain exhausts (router ≥ 4.10). */
374
+ providerSubCode?: string;
375
+ /** Short remediation hint from router enrichment (router ≥ 4.10). */
376
+ operatorHint?: string;
362
377
  /**
363
378
  * True when {@link mergeConfig} did not run (e.g. message-building threw first).
364
379
  * Routing facts may only reflect request.config / modelConfig, not flex-md defaults.
@@ -378,6 +393,8 @@ export type GatewayFallbackAttempt = {
378
393
  model?: string;
379
394
  httpStatus?: number;
380
395
  error: string;
396
+ /** Stable sub-code from `@x12i/ai-providers-router` ≥ 4.10 (`PROVIDER_AUTH_FAILED`, …). */
397
+ code?: string;
381
398
  responsePreview?: string;
382
399
  };
383
400
  /**
@@ -916,15 +933,11 @@ interface BaseLLMRequest extends Omit<LLMRequest, 'messages' | 'input' | 'reques
916
933
  */
917
934
  coreSkillId?: string;
918
935
  /**
919
- * Prompt template text (optional) — parsed with workingMemory and tier memories.
936
+ * Prompt template text (optional) — parsed with workingMemory and sent as the user turn.
937
+ * When omitted, workingMemory.input is serialized as the user message.
920
938
  * Use variables such as {{input}} resolved from workingMemory.input.
921
939
  */
922
940
  prompt?: string;
923
- /**
924
- * Context text (optional) - Template that can use workingMemory
925
- * Added as system message between instructions and prompt
926
- */
927
- context?: string;
928
941
  /**
929
942
  * Working memory (optional) - Affects all template parsing
930
943
  * Passed to Rendrix (v4+) for template variable substitution.
@@ -1025,7 +1038,7 @@ interface BaseLLMRequest extends Omit<LLMRequest, 'messages' | 'input' | 'reques
1025
1038
  * - agentId: Required to identify the agent
1026
1039
  * - instructions: Required (template text)
1027
1040
  *
1028
- * Minimum: instructions + prompt (prompt is required for user message)
1041
+ * Minimum: instructions + (prompt or workingMemory.input)
1029
1042
  *
1030
1043
  * Note: objectTypes is NOT supported in ChatRequest.
1031
1044
  * Use {@link AIInvokeRequest} with invoke() for structured output requests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-gateway",
3
- "version": "10.4.3",
3
+ "version": "11.0.0",
4
4
  "description": "AI Gateway - Unified interface for LLM provider routing and management",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,11 +44,11 @@
44
44
  "dependencies": {
45
45
  "@x12i/activix": "^9.0.2",
46
46
  "@x12i/ai-profiles": "^3.4.1",
47
- "@x12i/ai-providers-router": "^4.9.2",
47
+ "@x12i/ai-providers-router": "^4.10.0",
48
48
  "@x12i/ai-tools": "^3.3.5",
49
49
  "@x12i/flex-md": "^4.8.0",
50
50
  "@x12i/logxer": "^5.1.0",
51
- "@x12i/openrouter-runtime": "^1.0.3",
51
+ "@x12i/openrouter-runtime": "^1.0.4",
52
52
  "@x12i/rendrix": "^4.3.0"
53
53
  },
54
54
  "devDependencies": {