@x12i/ai-gateway 9.1.5 → 9.2.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.
@@ -120,6 +120,7 @@ export declare class ActivityManager {
120
120
  */
121
121
  logSuccess(activity: ActivityMetadata | undefined, details: {
122
122
  cost?: number;
123
+ costStatus?: 'priced' | 'unpriced';
123
124
  response: any;
124
125
  endTime: number;
125
126
  duration: number;
@@ -155,6 +155,12 @@ function pickActivixCompletionRoutingMetadata(response) {
155
155
  if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
156
156
  out.effectiveModelConfig = m.effectiveModelConfig;
157
157
  }
158
+ if (typeof m.cost === 'number' && Number.isFinite(m.cost))
159
+ out.cost = m.cost;
160
+ if (typeof m.costUsd === 'number' && Number.isFinite(m.costUsd))
161
+ out.costUsd = m.costUsd;
162
+ if (m.costStatus === 'priced' || m.costStatus === 'unpriced')
163
+ out.costStatus = m.costStatus;
158
164
  return out;
159
165
  }
160
166
  function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
@@ -844,6 +850,7 @@ export class ActivityManager {
844
850
  }
845
851
  await this.activix.completeRecord(activity.activityId, {
846
852
  cost: details.cost,
853
+ ...(details.costStatus ? { costStatus: details.costStatus } : {}),
847
854
  response: details.response,
848
855
  outer: {
849
856
  output: details.response,
@@ -43,6 +43,11 @@ export declare function extractJsonFromFlexMd(content: string, logger?: Logxer):
43
43
  json: any;
44
44
  method: string;
45
45
  } | null>;
46
+ /**
47
+ * Section-based markdown → camelCase field map (e.g. `### Short Answer` → `shortAnswer`).
48
+ * Used when output contracts need structured `parsed` fields but flex-md returned only `rawText`.
49
+ */
50
+ export declare function parseMarkdownSectionsFromContent(content: string, logger?: Logxer): Record<string, unknown>;
46
51
  /**
47
52
  * Check if flex-md module is available
48
53
  */
@@ -503,6 +503,22 @@ function fallbackMarkdownParser(content, logger) {
503
503
  method: 'fallback-raw-text'
504
504
  };
505
505
  }
506
+ /**
507
+ * Section-based markdown → camelCase field map (e.g. `### Short Answer` → `shortAnswer`).
508
+ * Used when output contracts need structured `parsed` fields but flex-md returned only `rawText`.
509
+ */
510
+ export function parseMarkdownSectionsFromContent(content, logger) {
511
+ const parsed = fallbackMarkdownParser(content, logger);
512
+ const json = parsed.json;
513
+ if (json != null && typeof json === 'object' && !Array.isArray(json)) {
514
+ const keys = Object.keys(json);
515
+ if (keys.length === 1 && keys[0] === 'rawText') {
516
+ return {};
517
+ }
518
+ return json;
519
+ }
520
+ return {};
521
+ }
506
522
  /**
507
523
  * Fallback JSON extraction when flex-md is not available
508
524
  */
@@ -43,6 +43,35 @@ export declare function extractTokenUsageFromRouterResponse(routerResponse: unkn
43
43
  * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
44
44
  */
45
45
  export declare function extractCostUsdFromRouterResponse(routerResponse: unknown): number | undefined;
46
+ /** Activity billing state when token usage is recorded (Run Analysis G8). */
47
+ export type ActivityCostStatus = 'priced' | 'unpriced';
48
+ export type ResolvedActivityCost = {
49
+ cost?: number;
50
+ costStatus?: ActivityCostStatus;
51
+ };
52
+ export declare function hasNonZeroTokenUsage(tokens: {
53
+ prompt: number;
54
+ completion: number;
55
+ total: number;
56
+ }): boolean;
57
+ /**
58
+ * Gateway fallback when the router does not set `metadata.costStatus`.
59
+ * Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
60
+ */
61
+ export declare function resolveActivityCostCompletion(tokens: {
62
+ prompt: number;
63
+ completion: number;
64
+ total: number;
65
+ }, costUsd: number | undefined): ResolvedActivityCost;
66
+ /**
67
+ * Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
68
+ * otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
69
+ */
70
+ export declare function resolveCostCompletionForActivity(routerResponse: unknown, tokens: {
71
+ prompt: number;
72
+ completion: number;
73
+ total: number;
74
+ }): ResolvedActivityCost;
46
75
  /**
47
76
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
48
77
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
@@ -315,6 +315,50 @@ export function extractCostUsdFromRouterResponse(routerResponse) {
315
315
  }
316
316
  return undefined;
317
317
  }
318
+ export function hasNonZeroTokenUsage(tokens) {
319
+ return !!(tokens.prompt || tokens.completion || tokens.total);
320
+ }
321
+ function pickRouterCostStatus(routerResponse) {
322
+ if (routerResponse == null || typeof routerResponse !== 'object')
323
+ return undefined;
324
+ const r = routerResponse;
325
+ const meta = r.metadata != null && typeof r.metadata === 'object'
326
+ ? r.metadata
327
+ : undefined;
328
+ const status = meta?.costStatus ?? r.costStatus;
329
+ return status === 'priced' || status === 'unpriced' ? status : undefined;
330
+ }
331
+ /**
332
+ * Gateway fallback when the router does not set `metadata.costStatus`.
333
+ * Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
334
+ */
335
+ export function resolveActivityCostCompletion(tokens, costUsd) {
336
+ if (typeof costUsd === 'number' && Number.isFinite(costUsd)) {
337
+ return { cost: costUsd, costStatus: 'priced' };
338
+ }
339
+ if (hasNonZeroTokenUsage(tokens)) {
340
+ return { costStatus: 'unpriced' };
341
+ }
342
+ return {};
343
+ }
344
+ /**
345
+ * Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
346
+ * otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
347
+ */
348
+ export function resolveCostCompletionForActivity(routerResponse, tokens) {
349
+ const routerStatus = pickRouterCostStatus(routerResponse);
350
+ const costUsd = extractCostUsdFromRouterResponse(routerResponse);
351
+ if (routerStatus === 'priced') {
352
+ return {
353
+ ...(typeof costUsd === 'number' && Number.isFinite(costUsd) ? { cost: costUsd } : {}),
354
+ costStatus: 'priced'
355
+ };
356
+ }
357
+ if (routerStatus === 'unpriced') {
358
+ return { costStatus: 'unpriced' };
359
+ }
360
+ return resolveActivityCostCompletion(tokens, costUsd);
361
+ }
318
362
  /**
319
363
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
320
364
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
package/dist/gateway.js CHANGED
@@ -8,7 +8,8 @@ import { ensureGatewayRequestIdentity } from './activity-manager.js';
8
8
  import { initializeGatewayComponents } from './gateway-config.js';
9
9
  import { buildMessages } from './message-builder.js';
10
10
  import { extractJsonFromFlexMd } from './flex-md-loader.js';
11
- import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
11
+ import { enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
12
+ import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, resolveCostCompletionForActivity, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
12
13
  import { autoRegisterProviders } from './gateway-provider-auto-register.js';
13
14
  import { setGatewayLastJobId, setGatewayRuntimeClients } from './runtime-objects.js';
14
15
  import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
@@ -108,8 +109,9 @@ export class AIGateway {
108
109
  },
109
110
  mode: 'sync'
110
111
  });
111
- const costUsdChat = extractCostUsdFromRouterResponse(response);
112
112
  const metaChat = response?.metadata || {};
113
+ const tokensChat = extractTokenUsageFromRouterResponse(response);
114
+ const costCompletionChat = resolveCostCompletionForActivity(response, tokensChat);
113
115
  // Create enhanced response
114
116
  const enhancedResponse = {
115
117
  content: response.content || '',
@@ -117,22 +119,25 @@ export class AIGateway {
117
119
  aiRequestId: request.aiRequestId,
118
120
  identity: request.identity,
119
121
  latencyMs: Date.now() - startTime,
120
- tokens: extractTokenUsageFromRouterResponse(response),
122
+ tokens: tokensChat,
121
123
  taskTypeId,
122
124
  agentType: 'chat',
123
- ...(typeof costUsdChat === 'number'
125
+ ...(costCompletionChat.costStatus === 'priced'
124
126
  ? {
125
- costUsd: costUsdChat,
126
- ...(typeof metaChat.cost === 'number' ? { cost: metaChat.cost } : { cost: costUsdChat })
127
+ costUsd: costCompletionChat.cost,
128
+ ...(typeof metaChat.cost === 'number'
129
+ ? { cost: metaChat.cost }
130
+ : { cost: costCompletionChat.cost })
127
131
  }
128
- : {})
132
+ : {}),
133
+ ...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {})
129
134
  }
130
135
  };
131
136
  // Track activity success if activity was started
132
137
  if (activity) {
133
138
  try {
134
139
  await this.activityManager.logSuccess(activity, {
135
- ...(typeof costUsdChat === 'number' ? { cost: costUsdChat } : {}),
140
+ ...costCompletionChat,
136
141
  response: enhancedResponse,
137
142
  endTime: Date.now(),
138
143
  duration: Date.now() - startTime
@@ -523,6 +528,8 @@ export class AIGateway {
523
528
  }
524
529
  contentType = 'structured';
525
530
  parsingMethod = 'flex-md';
531
+ const outputContractKeys = resolveOutputContractFieldKeys(request);
532
+ parsedContent = await enrichParsedContentForOutputContract(parsedContent, content, outputContractKeys, this.logger);
526
533
  let tokens = extractTokenUsageFromRouterResponse(routerResponse);
527
534
  if (!(tokens.prompt || tokens.completion || tokens.total)) {
528
535
  const alt = routerResponse?.rawResponse ?? routerResponse?.raw;
@@ -532,7 +539,7 @@ export class AIGateway {
532
539
  tokens = second;
533
540
  }
534
541
  }
535
- const resolvedCostUsd = extractCostUsdFromRouterResponse(routerResponse);
542
+ const costCompletion = resolveCostCompletionForActivity(routerResponse, tokens);
536
543
  const routerMetaForCost = routerResponse?.metadata || {};
537
544
  const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
538
545
  const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
@@ -551,14 +558,15 @@ export class AIGateway {
551
558
  parsingMethod,
552
559
  ...routingMetadataSlice,
553
560
  ...(effectiveModelConfig !== undefined ? { effectiveModelConfig } : {}),
554
- ...(typeof resolvedCostUsd === 'number'
561
+ ...(costCompletion.costStatus === 'priced'
555
562
  ? {
556
- costUsd: resolvedCostUsd,
563
+ costUsd: costCompletion.cost,
557
564
  ...(typeof routerMetaForCost.cost === 'number'
558
565
  ? { cost: routerMetaForCost.cost }
559
- : { cost: resolvedCostUsd })
566
+ : { cost: costCompletion.cost })
560
567
  }
561
568
  : {}),
569
+ ...(costCompletion.costStatus ? { costStatus: costCompletion.costStatus } : {}),
562
570
  ...(traceEnabled
563
571
  ? {
564
572
  requestIds: traceRequestIds,
@@ -597,7 +605,7 @@ export class AIGateway {
597
605
  usage: tokens
598
606
  };
599
607
  await this.activityManager.logSuccess(activity, {
600
- ...(typeof resolvedCostUsd === 'number' ? { cost: resolvedCostUsd } : {}),
608
+ ...costCompletion,
601
609
  response: activityResponse,
602
610
  endTime: Date.now(),
603
611
  duration: Date.now() - startTime
package/dist/index.d.ts CHANGED
@@ -17,9 +17,14 @@ export { AIGateway } from './gateway.js';
17
17
  export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
18
18
  export { autoRegisterProviders } from './gateway-provider-auto-register.js';
19
19
  export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions } from './types.js';
20
- export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, hasNonZeroTokenUsage } from './gateway-utils.js';
21
+ export type { ActivityCostStatus, ResolvedActivityCost } from './gateway-utils.js';
22
+ export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
23
+ export type { OutputContractSpec } from './output-contract-normalizer.js';
21
24
  export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
22
25
  export type { GatewayTemplateRenderRequestSlice } from './template-render-merge.js';
26
+ export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
27
+ export type { GatewayDualMemoryRoot } from './memory-path-resolution.js';
23
28
  export type { UsageTier } from './types.js';
24
29
  export { Activix } from '@x12i/activix';
25
30
  export type { ActivixRunContext, FindByRunContextCriteria, GetJobActivitiesInput, GetJobActivitiesResult } from '@x12i/activix';
package/dist/index.js CHANGED
@@ -17,8 +17,10 @@ export * from '@x12i/ai-providers-router';
17
17
  export { AIGateway } from './gateway.js';
18
18
  export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
19
19
  export { autoRegisterProviders } from './gateway-provider-auto-register.js';
20
- export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, hasNonZeroTokenUsage } from './gateway-utils.js';
21
+ export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
21
22
  export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
23
+ export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
22
24
  // Usage tracking: UsageTracker class methods are available but consumption calculation is disabled
23
25
  // (x-models was previously used for RPM/TPM tracking but is no longer integrated)
24
26
  // Re-export activity tracking primitives (Activix)
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Dual memory roots `input` (merged MAIN payload) and `inputs` (caller / graph-entry bag).
3
+ * Aligned with @exellix/graph-engine ≥ 5.5.0 resolution rules for template / smart-input paths.
4
+ */
5
+ export declare const GATEWAY_DUAL_MEMORY_ROOTS: readonly ["input", "inputs"];
6
+ export type GatewayDualMemoryRoot = (typeof GATEWAY_DUAL_MEMORY_ROOTS)[number];
7
+ /**
8
+ * Parse JSON object shapes from strings (matches graph-engine `parseLooseJsonObject`).
9
+ */
10
+ export declare function parseLooseJsonObject(value: unknown): Record<string, unknown> | undefined;
11
+ /** Caller / graph-entry bag from `workingMemory.inputs`. */
12
+ export declare function extractCallerInputsBag(workingMemory: unknown): Record<string, unknown> | undefined;
13
+ /** Merged MAIN bucket from `workingMemory.input` (object or parsed JSON string). */
14
+ export declare function coalesceMergedInputBucket(workingMemory: unknown): unknown;
15
+ /**
16
+ * Resolve a dotted path against working memory with dual-root rules:
17
+ * - `inputs` / `inputs.*` → caller bag first, then merged `input`
18
+ * - `input` / `input.*` → merged `input` first, then caller `inputs`
19
+ * - other paths → direct lookup on working memory
20
+ */
21
+ export declare function resolveGatewayMemoryPathValue(workingMemory: unknown, path: string): unknown;
22
+ /**
23
+ * Working-memory view for Rendrix / smart-input: preserves all keys but overlays
24
+ * `input` and `inputs` with dual-root merge views (does not rewrite authored paths).
25
+ */
26
+ export declare function buildMemoryResolutionRootFromWorkingMemory(workingMemory: unknown): Record<string, unknown>;
27
+ /**
28
+ * When WM carries `input` and/or `inputs`, return a resolution root for template rendering.
29
+ * Otherwise returns the original reference unchanged.
30
+ */
31
+ export declare function prepareWorkingMemoryForTemplateRender(workingMemory: unknown): unknown;
32
+ /** Optional migration: rewrite `inputs.*` smart-input / memory paths to `input.*`. */
33
+ export declare function mapSmartInputPathsInputsToInput(paths: string[]): string[];
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Dual memory roots `input` (merged MAIN payload) and `inputs` (caller / graph-entry bag).
3
+ * Aligned with @exellix/graph-engine ≥ 5.5.0 resolution rules for template / smart-input paths.
4
+ */
5
+ export const GATEWAY_DUAL_MEMORY_ROOTS = ['input', 'inputs'];
6
+ function isPlainObject(value) {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ }
9
+ /**
10
+ * Parse JSON object shapes from strings (matches graph-engine `parseLooseJsonObject`).
11
+ */
12
+ export function parseLooseJsonObject(value) {
13
+ if (isPlainObject(value))
14
+ return value;
15
+ if (typeof value !== 'string')
16
+ return undefined;
17
+ const trimmed = value.trim();
18
+ if (!trimmed.startsWith('{'))
19
+ return undefined;
20
+ try {
21
+ const parsed = JSON.parse(trimmed);
22
+ return isPlainObject(parsed) ? parsed : undefined;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ /** Caller / graph-entry bag from `workingMemory.inputs`. */
29
+ export function extractCallerInputsBag(workingMemory) {
30
+ if (!isPlainObject(workingMemory))
31
+ return undefined;
32
+ const inputs = workingMemory.inputs;
33
+ return isPlainObject(inputs) ? inputs : undefined;
34
+ }
35
+ /** Merged MAIN bucket from `workingMemory.input` (object or parsed JSON string). */
36
+ export function coalesceMergedInputBucket(workingMemory) {
37
+ if (!isPlainObject(workingMemory))
38
+ return undefined;
39
+ const raw = workingMemory.input;
40
+ if (raw === undefined || raw === null)
41
+ return undefined;
42
+ const parsed = parseLooseJsonObject(raw);
43
+ return parsed !== undefined ? parsed : raw;
44
+ }
45
+ function getValueAtPath(obj, path) {
46
+ if (obj == null)
47
+ return undefined;
48
+ if (path === '' || path === 'this' || path === '.')
49
+ return obj;
50
+ const parts = path.split('.');
51
+ let cur = obj;
52
+ for (const part of parts) {
53
+ if (cur == null || typeof cur !== 'object')
54
+ return undefined;
55
+ cur = cur[part];
56
+ }
57
+ return cur;
58
+ }
59
+ function asObjectBucket(value) {
60
+ const parsed = parseLooseJsonObject(value);
61
+ if (parsed)
62
+ return parsed;
63
+ return isPlainObject(value) ? value : undefined;
64
+ }
65
+ /**
66
+ * Shallow-deep merge for template lookup: primary wins per key; nested plain objects merge recursively.
67
+ */
68
+ function mergeBucketViews(primary, fallback) {
69
+ const pObj = asObjectBucket(primary);
70
+ const fObj = asObjectBucket(fallback);
71
+ if (!pObj && !fObj) {
72
+ if (primary !== undefined && primary !== null)
73
+ return primary;
74
+ return fallback;
75
+ }
76
+ if (!pObj)
77
+ return { ...fObj };
78
+ if (!fObj)
79
+ return { ...pObj };
80
+ const keys = new Set([...Object.keys(pObj), ...Object.keys(fObj)]);
81
+ const view = {};
82
+ for (const key of keys) {
83
+ const pv = pObj[key];
84
+ const fv = fObj[key];
85
+ if (isPlainObject(pv) && isPlainObject(fv)) {
86
+ view[key] = mergeBucketViews(pv, fv);
87
+ }
88
+ else {
89
+ view[key] = pv !== undefined ? pv : fv;
90
+ }
91
+ }
92
+ return view;
93
+ }
94
+ /**
95
+ * Resolve a dotted path against working memory with dual-root rules:
96
+ * - `inputs` / `inputs.*` → caller bag first, then merged `input`
97
+ * - `input` / `input.*` → merged `input` first, then caller `inputs`
98
+ * - other paths → direct lookup on working memory
99
+ */
100
+ export function resolveGatewayMemoryPathValue(workingMemory, path) {
101
+ const trimmed = path.trim();
102
+ if (!trimmed)
103
+ return undefined;
104
+ const dot = trimmed.indexOf('.');
105
+ const root = dot >= 0 ? trimmed.slice(0, dot) : trimmed;
106
+ const tail = dot >= 0 ? trimmed.slice(dot + 1) : '';
107
+ if (root === 'inputs') {
108
+ const primary = extractCallerInputsBag(workingMemory);
109
+ const fallback = coalesceMergedInputBucket(workingMemory);
110
+ if (!tail) {
111
+ if (primary && Object.keys(primary).length > 0)
112
+ return primary;
113
+ return fallback;
114
+ }
115
+ const fromPrimary = primary ? getValueAtPath(primary, tail) : undefined;
116
+ if (fromPrimary !== undefined)
117
+ return fromPrimary;
118
+ return fallback !== undefined ? getValueAtPath(fallback, tail) : undefined;
119
+ }
120
+ if (root === 'input') {
121
+ const primary = coalesceMergedInputBucket(workingMemory);
122
+ const fallback = extractCallerInputsBag(workingMemory);
123
+ if (!tail) {
124
+ if (primary !== undefined && primary !== null)
125
+ return primary;
126
+ return fallback;
127
+ }
128
+ const fromPrimary = primary !== undefined ? getValueAtPath(primary, tail) : undefined;
129
+ if (fromPrimary !== undefined)
130
+ return fromPrimary;
131
+ return fallback ? getValueAtPath(fallback, tail) : undefined;
132
+ }
133
+ return getValueAtPath(workingMemory, trimmed);
134
+ }
135
+ /**
136
+ * Working-memory view for Rendrix / smart-input: preserves all keys but overlays
137
+ * `input` and `inputs` with dual-root merge views (does not rewrite authored paths).
138
+ */
139
+ export function buildMemoryResolutionRootFromWorkingMemory(workingMemory) {
140
+ if (!isPlainObject(workingMemory))
141
+ return {};
142
+ const inputsBag = extractCallerInputsBag(workingMemory);
143
+ const mergedInput = coalesceMergedInputBucket(workingMemory);
144
+ return {
145
+ ...workingMemory,
146
+ input: mergeBucketViews(mergedInput, inputsBag),
147
+ inputs: mergeBucketViews(inputsBag, mergedInput)
148
+ };
149
+ }
150
+ /**
151
+ * When WM carries `input` and/or `inputs`, return a resolution root for template rendering.
152
+ * Otherwise returns the original reference unchanged.
153
+ */
154
+ export function prepareWorkingMemoryForTemplateRender(workingMemory) {
155
+ if (!isPlainObject(workingMemory))
156
+ return workingMemory;
157
+ if (workingMemory.input === undefined && workingMemory.inputs === undefined) {
158
+ return workingMemory;
159
+ }
160
+ return buildMemoryResolutionRootFromWorkingMemory(workingMemory);
161
+ }
162
+ /** Optional migration: rewrite `inputs.*` smart-input / memory paths to `input.*`. */
163
+ export function mapSmartInputPathsInputsToInput(paths) {
164
+ return paths.map((p) => {
165
+ const t = p.trim();
166
+ if (t === 'inputs')
167
+ return 'input';
168
+ if (t.startsWith('inputs.'))
169
+ return `input.${t.slice('inputs.'.length)}`;
170
+ return p;
171
+ });
172
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
3
+ * Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
4
+ */
5
+ import type { Logxer } from '@x12i/logxer';
6
+ /** Explicit contract: field names or JSON-schema-style `properties` only. */
7
+ export type OutputContractSpec = string[] | {
8
+ properties: Record<string, unknown>;
9
+ };
10
+ /**
11
+ * Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
12
+ */
13
+ export declare function contractSpecToFieldKeys(contract: unknown): string[];
14
+ /**
15
+ * Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
16
+ */
17
+ export declare function resolveOutputContractFieldKeys(request: unknown): string[] | undefined;
18
+ /**
19
+ * Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
20
+ */
21
+ export declare function enrichParsedContentForOutputContract(parsed: unknown, rawContent: string, contractKeys: string[] | undefined, logger?: Logxer): Promise<Record<string, unknown>>;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
3
+ * Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
4
+ */
5
+ import { parseMarkdownSectionsFromContent } from './flex-md-loader.js';
6
+ import { coalesceMergedInputBucket, extractCallerInputsBag, parseLooseJsonObject } from './memory-path-resolution.js';
7
+ function isPlainObject(value) {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+ function hasMeaningfulContractValue(value) {
11
+ if (value === undefined || value === null)
12
+ return false;
13
+ if (typeof value === 'string')
14
+ return value.trim().length > 0;
15
+ if (Array.isArray(value))
16
+ return value.length > 0;
17
+ if (typeof value === 'object')
18
+ return Object.keys(value).length > 0;
19
+ return true;
20
+ }
21
+ /**
22
+ * Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
23
+ */
24
+ export function contractSpecToFieldKeys(contract) {
25
+ if (contract == null)
26
+ return [];
27
+ if (Array.isArray(contract)) {
28
+ return contract.filter((k) => typeof k === 'string' && k.trim().length > 0);
29
+ }
30
+ if (!isPlainObject(contract))
31
+ return [];
32
+ const properties = contract.properties;
33
+ if (isPlainObject(properties)) {
34
+ return Object.keys(properties);
35
+ }
36
+ return [];
37
+ }
38
+ /** Graph-engine path: `workingMemory.inputs.outputContract` or merged `input.outputContract`. */
39
+ function readExplicitOutputContractFromWorkingMemory(workingMemory) {
40
+ if (!isPlainObject(workingMemory))
41
+ return undefined;
42
+ const inputs = extractCallerInputsBag(workingMemory);
43
+ if (inputs?.outputContract !== undefined)
44
+ return inputs.outputContract;
45
+ const input = coalesceMergedInputBucket(workingMemory);
46
+ if (isPlainObject(input) && input.outputContract !== undefined) {
47
+ return input.outputContract;
48
+ }
49
+ const loose = parseLooseJsonObject(workingMemory.input);
50
+ if (loose?.outputContract !== undefined)
51
+ return loose.outputContract;
52
+ return undefined;
53
+ }
54
+ /**
55
+ * Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
56
+ */
57
+ export function resolveOutputContractFieldKeys(request) {
58
+ if (request == null || typeof request !== 'object')
59
+ return undefined;
60
+ const r = request;
61
+ const candidates = [
62
+ r.outputContract,
63
+ readExplicitOutputContractFromWorkingMemory(r.workingMemory)
64
+ ];
65
+ for (const candidate of candidates) {
66
+ const keys = contractSpecToFieldKeys(candidate);
67
+ if (keys.length > 0)
68
+ return keys;
69
+ }
70
+ return undefined;
71
+ }
72
+ function asParsedRecord(parsed) {
73
+ if (!isPlainObject(parsed))
74
+ return {};
75
+ return { ...parsed };
76
+ }
77
+ function pickAliasValue(source, key) {
78
+ if (hasMeaningfulContractValue(source[key]))
79
+ return source[key];
80
+ const spaced = key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
81
+ const title = spaced.charAt(0).toUpperCase() + spaced.slice(1);
82
+ const aliases = [
83
+ key,
84
+ key.toLowerCase(),
85
+ title,
86
+ title.replace(/\s+/g, ' '),
87
+ key.replace(/([A-Z])/g, '_$1').toLowerCase()
88
+ ];
89
+ for (const alias of aliases) {
90
+ if (hasMeaningfulContractValue(source[alias]))
91
+ return source[alias];
92
+ }
93
+ return undefined;
94
+ }
95
+ /**
96
+ * Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
97
+ */
98
+ export async function enrichParsedContentForOutputContract(parsed, rawContent, contractKeys, logger) {
99
+ const base = asParsedRecord(parsed);
100
+ if (!contractKeys?.length)
101
+ return base;
102
+ const missing = contractKeys.filter((k) => !hasMeaningfulContractValue(base[k]));
103
+ if (missing.length === 0)
104
+ return base;
105
+ const content = typeof rawContent === 'string' && rawContent.trim().length > 0
106
+ ? rawContent
107
+ : typeof base.rawText === 'string'
108
+ ? base.rawText
109
+ : '';
110
+ if (!content.trim())
111
+ return base;
112
+ const fromMarkdown = parseMarkdownSectionsFromContent(content, logger);
113
+ const merged = { ...base };
114
+ for (const key of missing) {
115
+ const value = pickAliasValue(fromMarkdown, key);
116
+ if (hasMeaningfulContractValue(value)) {
117
+ merged[key] = value;
118
+ }
119
+ }
120
+ return merged;
121
+ }