@x12i/ai-gateway 7.9.1

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.
Files changed (179) hide show
  1. package/README.md +4259 -0
  2. package/config.defaults.json +31 -0
  3. package/dist/activity-manager.d.ts +206 -0
  4. package/dist/activity-manager.js +1051 -0
  5. package/dist/config/activity-tracking-config.d.ts +11 -0
  6. package/dist/config/activity-tracking-config.js +15 -0
  7. package/dist/config.defaults.json +31 -0
  8. package/dist/content-normalizer/content-normalizer.d.ts +46 -0
  9. package/dist/content-normalizer/content-normalizer.js +393 -0
  10. package/dist/content-normalizer/index.d.ts +7 -0
  11. package/dist/content-normalizer/index.js +6 -0
  12. package/dist/content-normalizer/types.d.ts +33 -0
  13. package/dist/content-normalizer/types.js +4 -0
  14. package/dist/defaults/instructions-blocks.json +61 -0
  15. package/dist/defaults/model-config.json +16 -0
  16. package/dist/defaults/template-rendering.json +6 -0
  17. package/dist/flex-md-loader.d.ts +109 -0
  18. package/dist/flex-md-loader.js +940 -0
  19. package/dist/gateway-config.d.ts +49 -0
  20. package/dist/gateway-config.js +292 -0
  21. package/dist/gateway-conversion.d.ts +29 -0
  22. package/dist/gateway-conversion.js +174 -0
  23. package/dist/gateway-instructions.d.ts +30 -0
  24. package/dist/gateway-instructions.js +62 -0
  25. package/dist/gateway-memory.d.ts +51 -0
  26. package/dist/gateway-memory.js +207 -0
  27. package/dist/gateway-messages.d.ts +23 -0
  28. package/dist/gateway-messages.js +83 -0
  29. package/dist/gateway-meta.d.ts +25 -0
  30. package/dist/gateway-meta.js +87 -0
  31. package/dist/gateway-provider-auto-register.d.ts +17 -0
  32. package/dist/gateway-provider-auto-register.js +159 -0
  33. package/dist/gateway-provider.d.ts +54 -0
  34. package/dist/gateway-provider.js +202 -0
  35. package/dist/gateway-rate-limiter-constants.d.ts +16 -0
  36. package/dist/gateway-rate-limiter-constants.js +16 -0
  37. package/dist/gateway-rate-limiter.d.ts +56 -0
  38. package/dist/gateway-rate-limiter.js +107 -0
  39. package/dist/gateway-retry.d.ts +49 -0
  40. package/dist/gateway-retry.js +204 -0
  41. package/dist/gateway-utils.d.ts +21 -0
  42. package/dist/gateway-utils.js +181 -0
  43. package/dist/gateway-validation.d.ts +13 -0
  44. package/dist/gateway-validation.js +50 -0
  45. package/dist/gateway.d.ts +39 -0
  46. package/dist/gateway.js +430 -0
  47. package/dist/index.d.ts +36 -0
  48. package/dist/index.js +55 -0
  49. package/dist/instruction-errors.d.ts +16 -0
  50. package/dist/instruction-errors.js +29 -0
  51. package/dist/instruction-optimizer.d.ts +113 -0
  52. package/dist/instruction-optimizer.js +293 -0
  53. package/dist/instructions-parser.d.ts +31 -0
  54. package/dist/instructions-parser.js +56 -0
  55. package/dist/logger-factory.d.ts +17 -0
  56. package/dist/logger-factory.js +42 -0
  57. package/dist/message-builder.d.ts +41 -0
  58. package/dist/message-builder.js +522 -0
  59. package/dist/object-types-library-integration.d.ts +22 -0
  60. package/dist/object-types-library-integration.js +27 -0
  61. package/dist/object-types-library.d.ts +351 -0
  62. package/dist/object-types-library.js +210 -0
  63. package/dist/output-auditor.d.ts +44 -0
  64. package/dist/output-auditor.js +49 -0
  65. package/dist/request-report-generator.d.ts +60 -0
  66. package/dist/request-report-generator.js +169 -0
  67. package/dist/response-analyzer/format-type-detector.d.ts +35 -0
  68. package/dist/response-analyzer/format-type-detector.js +115 -0
  69. package/dist/response-analyzer/index.d.ts +9 -0
  70. package/dist/response-analyzer/index.js +8 -0
  71. package/dist/response-analyzer/object-type-detector.d.ts +42 -0
  72. package/dist/response-analyzer/object-type-detector.js +95 -0
  73. package/dist/response-analyzer/response-analyzer.d.ts +38 -0
  74. package/dist/response-analyzer/response-analyzer.js +97 -0
  75. package/dist/response-analyzer/types.d.ts +97 -0
  76. package/dist/response-analyzer/types.js +4 -0
  77. package/dist/response-fallback-fixer.d.ts +11 -0
  78. package/dist/response-fallback-fixer.js +123 -0
  79. package/dist/runtime-objects.d.ts +52 -0
  80. package/dist/runtime-objects.js +46 -0
  81. package/dist/template-parser.d.ts +58 -0
  82. package/dist/template-parser.js +99 -0
  83. package/dist/template-render-merge.d.ts +9 -0
  84. package/dist/template-render-merge.js +40 -0
  85. package/dist/troubleshooting-helper.d.ts +123 -0
  86. package/dist/troubleshooting-helper.js +596 -0
  87. package/dist/types.d.ts +1173 -0
  88. package/dist/types.js +6 -0
  89. package/dist/usage-tracker.d.ts +78 -0
  90. package/dist/usage-tracker.js +79 -0
  91. package/dist-cjs/activity-manager.cjs +1056 -0
  92. package/dist-cjs/activity-manager.d.ts +206 -0
  93. package/dist-cjs/config/activity-tracking-config.cjs +18 -0
  94. package/dist-cjs/config/activity-tracking-config.d.ts +11 -0
  95. package/dist-cjs/config.defaults.json +31 -0
  96. package/dist-cjs/content-normalizer/content-normalizer.cjs +398 -0
  97. package/dist-cjs/content-normalizer/content-normalizer.d.ts +46 -0
  98. package/dist-cjs/content-normalizer/index.cjs +12 -0
  99. package/dist-cjs/content-normalizer/index.d.ts +7 -0
  100. package/dist-cjs/content-normalizer/types.cjs +5 -0
  101. package/dist-cjs/content-normalizer/types.d.ts +33 -0
  102. package/dist-cjs/defaults/instructions-blocks.json +61 -0
  103. package/dist-cjs/defaults/model-config.json +16 -0
  104. package/dist-cjs/defaults/template-rendering.json +6 -0
  105. package/dist-cjs/flex-md-loader.cjs +986 -0
  106. package/dist-cjs/flex-md-loader.d.ts +109 -0
  107. package/dist-cjs/gateway-config.cjs +331 -0
  108. package/dist-cjs/gateway-config.d.ts +49 -0
  109. package/dist-cjs/gateway-conversion.cjs +212 -0
  110. package/dist-cjs/gateway-conversion.d.ts +29 -0
  111. package/dist-cjs/gateway-instructions.cjs +67 -0
  112. package/dist-cjs/gateway-instructions.d.ts +30 -0
  113. package/dist-cjs/gateway-memory.cjs +211 -0
  114. package/dist-cjs/gateway-memory.d.ts +51 -0
  115. package/dist-cjs/gateway-messages.cjs +86 -0
  116. package/dist-cjs/gateway-messages.d.ts +23 -0
  117. package/dist-cjs/gateway-meta.cjs +90 -0
  118. package/dist-cjs/gateway-meta.d.ts +25 -0
  119. package/dist-cjs/gateway-provider-auto-register.cjs +195 -0
  120. package/dist-cjs/gateway-provider-auto-register.d.ts +17 -0
  121. package/dist-cjs/gateway-provider.cjs +214 -0
  122. package/dist-cjs/gateway-provider.d.ts +54 -0
  123. package/dist-cjs/gateway-rate-limiter-constants.cjs +19 -0
  124. package/dist-cjs/gateway-rate-limiter-constants.d.ts +16 -0
  125. package/dist-cjs/gateway-rate-limiter.cjs +111 -0
  126. package/dist-cjs/gateway-rate-limiter.d.ts +56 -0
  127. package/dist-cjs/gateway-retry.cjs +212 -0
  128. package/dist-cjs/gateway-retry.d.ts +49 -0
  129. package/dist-cjs/gateway-utils.cjs +219 -0
  130. package/dist-cjs/gateway-utils.d.ts +21 -0
  131. package/dist-cjs/gateway-validation.cjs +54 -0
  132. package/dist-cjs/gateway-validation.d.ts +13 -0
  133. package/dist-cjs/gateway.cjs +434 -0
  134. package/dist-cjs/gateway.d.ts +39 -0
  135. package/dist-cjs/index.cjs +108 -0
  136. package/dist-cjs/index.d.ts +36 -0
  137. package/dist-cjs/instruction-errors.cjs +34 -0
  138. package/dist-cjs/instruction-errors.d.ts +16 -0
  139. package/dist-cjs/instruction-optimizer.cjs +299 -0
  140. package/dist-cjs/instruction-optimizer.d.ts +113 -0
  141. package/dist-cjs/instructions-parser.cjs +61 -0
  142. package/dist-cjs/instructions-parser.d.ts +31 -0
  143. package/dist-cjs/logger-factory.cjs +45 -0
  144. package/dist-cjs/logger-factory.d.ts +17 -0
  145. package/dist-cjs/message-builder.cjs +558 -0
  146. package/dist-cjs/message-builder.d.ts +41 -0
  147. package/dist-cjs/object-types-library-integration.cjs +32 -0
  148. package/dist-cjs/object-types-library-integration.d.ts +22 -0
  149. package/dist-cjs/object-types-library.cjs +215 -0
  150. package/dist-cjs/object-types-library.d.ts +351 -0
  151. package/dist-cjs/output-auditor.cjs +52 -0
  152. package/dist-cjs/output-auditor.d.ts +44 -0
  153. package/dist-cjs/request-report-generator.cjs +172 -0
  154. package/dist-cjs/request-report-generator.d.ts +60 -0
  155. package/dist-cjs/response-analyzer/format-type-detector.cjs +119 -0
  156. package/dist-cjs/response-analyzer/format-type-detector.d.ts +35 -0
  157. package/dist-cjs/response-analyzer/index.cjs +14 -0
  158. package/dist-cjs/response-analyzer/index.d.ts +9 -0
  159. package/dist-cjs/response-analyzer/object-type-detector.cjs +99 -0
  160. package/dist-cjs/response-analyzer/object-type-detector.d.ts +42 -0
  161. package/dist-cjs/response-analyzer/response-analyzer.cjs +101 -0
  162. package/dist-cjs/response-analyzer/response-analyzer.d.ts +38 -0
  163. package/dist-cjs/response-analyzer/types.cjs +5 -0
  164. package/dist-cjs/response-analyzer/types.d.ts +97 -0
  165. package/dist-cjs/response-fallback-fixer.cjs +126 -0
  166. package/dist-cjs/response-fallback-fixer.d.ts +11 -0
  167. package/dist-cjs/runtime-objects.cjs +52 -0
  168. package/dist-cjs/runtime-objects.d.ts +52 -0
  169. package/dist-cjs/template-parser.cjs +136 -0
  170. package/dist-cjs/template-parser.d.ts +58 -0
  171. package/dist-cjs/template-render-merge.cjs +43 -0
  172. package/dist-cjs/template-render-merge.d.ts +9 -0
  173. package/dist-cjs/troubleshooting-helper.cjs +611 -0
  174. package/dist-cjs/troubleshooting-helper.d.ts +123 -0
  175. package/dist-cjs/types.cjs +7 -0
  176. package/dist-cjs/types.d.ts +1173 -0
  177. package/dist-cjs/usage-tracker.cjs +83 -0
  178. package/dist-cjs/usage-tracker.d.ts +78 -0
  179. package/package.json +91 -0
@@ -0,0 +1,1051 @@
1
+ /**
2
+ * Activity Manager
3
+ *
4
+ * Manages activity tracking for LLM requests.
5
+ * Wraps the ActivityTracker and provides convenience methods.
6
+ */
7
+ import { Activix, activixActivityIo, activixOuterTier } from '@x12i/activix';
8
+ import { resolveActivityTrackingConfig } from './config/activity-tracking-config.js';
9
+ function readAiRequestIdFromRequest(request) {
10
+ const aiRequestId = request.aiRequestId;
11
+ if (typeof aiRequestId === 'string' && aiRequestId.trim().length > 0) {
12
+ return aiRequestId.trim();
13
+ }
14
+ throw new Error('aiRequestId is required and must be provided by upstream');
15
+ }
16
+ function trimIdentityString(v) {
17
+ return typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined;
18
+ }
19
+ function isMeaningfulIdentityValue(v) {
20
+ if (v === undefined || v === null)
21
+ return false;
22
+ if (typeof v === 'string')
23
+ return v.trim().length > 0;
24
+ return true;
25
+ }
26
+ function incomingIdentityHasKey(incoming, key) {
27
+ if (!incoming || !(key in incoming))
28
+ return false;
29
+ return isMeaningfulIdentityValue(incoming[key]);
30
+ }
31
+ /** Keys the gateway may supply only when absent on upstream `identity` (never overwrites roots). */
32
+ const ENRICHABLE_IDENTITY_KEYS = [
33
+ 'graphId',
34
+ 'nodeId',
35
+ 'masterSkillId',
36
+ 'masterSkillActivityId',
37
+ 'coreSkillId'
38
+ ];
39
+ function pickGatewayEnrichmentFields(enrichment) {
40
+ const out = {};
41
+ for (const key of ENRICHABLE_IDENTITY_KEYS) {
42
+ const v = enrichment[key];
43
+ if (isMeaningfulIdentityValue(v)) {
44
+ out[key] = v;
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+ /**
50
+ * Adds gateway-derived fields only when upstream `identity` did not already set them.
51
+ * Execution context must flow from upstream: inner layers extend, they do not replace outer `sessionId`,
52
+ * `instance`, graph linkage, or other fields already present on the incoming envelope.
53
+ */
54
+ function mergeActivixEnrichment(incoming, enrichment) {
55
+ const filtered = pickGatewayEnrichmentFields(enrichment);
56
+ const out = {};
57
+ for (const key of ENRICHABLE_IDENTITY_KEYS) {
58
+ if (incomingIdentityHasKey(incoming, key))
59
+ continue;
60
+ const v = filtered[key];
61
+ if (!isMeaningfulIdentityValue(v))
62
+ continue;
63
+ out[key] = v;
64
+ }
65
+ return out;
66
+ }
67
+ /**
68
+ * Graph/skill linkage fields merged into identity before Activix persistence (gateway enrichment).
69
+ */
70
+ function graphIdentityExtrasFromRequest(request) {
71
+ const aiRequest = request;
72
+ const graphId = aiRequest.masterSkillId || request.graphId;
73
+ const nodeId = aiRequest.skillId || aiRequest.coreSkillId || request.nodeId;
74
+ return {
75
+ ...(graphId && { graphId }),
76
+ ...(nodeId && { nodeId }),
77
+ ...(aiRequest.masterSkillId && { masterSkillId: aiRequest.masterSkillId }),
78
+ ...(aiRequest.masterSkillActivityId && { masterSkillActivityId: aiRequest.masterSkillActivityId })
79
+ };
80
+ }
81
+ /**
82
+ * Builds `instance` with required `instanceId` / `type` and preserves extra keys from upstream `identity.instance`.
83
+ */
84
+ function buildInstanceEnvelope(request) {
85
+ const raw = request.identity?.instance;
86
+ const incoming = raw != null && typeof raw === 'object' && !Array.isArray(raw) ? raw : undefined;
87
+ const incomingId = incoming && typeof incoming.instanceId === 'string' && incoming.instanceId.trim().length > 0
88
+ ? incoming.instanceId.trim()
89
+ : undefined;
90
+ const incomingType = incoming && typeof incoming.type === 'string' && incoming.type.trim().length > 0
91
+ ? incoming.type.trim()
92
+ : undefined;
93
+ const rest = incoming ? { ...incoming } : {};
94
+ delete rest.instanceId;
95
+ delete rest.type;
96
+ return {
97
+ ...rest,
98
+ instanceId: incomingId || request.agentId,
99
+ type: incomingType || request.agentType || 'unknown'
100
+ };
101
+ }
102
+ function resolveSessionIdForRequest(request, incomingIdentity, aiRequestId) {
103
+ const fromIdentity = trimIdentityString(incomingIdentity?.sessionId);
104
+ const topLevel = trimIdentityString(request.sessionId);
105
+ const jobId = trimIdentityString(request.jobId);
106
+ if (fromIdentity !== undefined && topLevel !== undefined && fromIdentity !== topLevel) {
107
+ throw new Error(`identity.sessionId (${fromIdentity}) and top-level sessionId (${topLevel}) must agree`);
108
+ }
109
+ return fromIdentity ?? topLevel ?? jobId ?? aiRequestId;
110
+ }
111
+ function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
112
+ const incomingIdentity = request.identity;
113
+ const sessionId = resolveSessionIdForRequest(request, incomingIdentity, aiRequestId);
114
+ const legacyJobId = trimIdentityString(incomingIdentity?.jobId) ?? trimIdentityString(request.jobId);
115
+ const combinedEnrichment = pickGatewayEnrichmentFields({
116
+ ...graphIdentityExtrasFromRequest(request),
117
+ ...(extras || {})
118
+ });
119
+ const safeEnrichment = mergeActivixEnrichment(incomingIdentity, combinedEnrichment);
120
+ // Request fields are defaults for this hop; incoming `identity` from upstream wins on conflict.
121
+ // Gateway enrichment (graph/skill, etc.) fills only keys the outer envelope did not already set.
122
+ const merged = {
123
+ jobTypeId: request.jobTypeId,
124
+ agentId: request.agentId,
125
+ taskId: request.taskId,
126
+ taskTypeId: request.taskTypeId,
127
+ ...safeEnrichment,
128
+ ...(incomingIdentity || {}),
129
+ sessionId,
130
+ instance: buildInstanceEnvelope(request),
131
+ aiRequestId
132
+ };
133
+ merged.sessionId = sessionId;
134
+ merged.instance = buildInstanceEnvelope(request);
135
+ merged.aiRequestId = aiRequestId;
136
+ if (legacyJobId !== undefined) {
137
+ merged.jobId = legacyJobId;
138
+ }
139
+ return merged;
140
+ }
141
+ /**
142
+ * Normalizes and attaches the gateway `identity` envelope on the request (Activix + router correlation).
143
+ * Call at the API boundary before the router when activity tracking is off, and used internally when it is on.
144
+ *
145
+ * **Execution context:** treats `request.identity` as received from upstream. Root fields (`sessionId`,
146
+ * `instance`, and any keys already set on that object) are not replaced by gateway defaults; the gateway
147
+ * only fills missing required pieces and adds local context (e.g. graph linkage) where absent.
148
+ */
149
+ export function ensureGatewayRequestIdentity(request, extras) {
150
+ const aiRequestId = readAiRequestIdFromRequest(request);
151
+ const identity = mergeGatewayActivityIdentity(request, aiRequestId, {
152
+ ...graphIdentityExtrasFromRequest(request),
153
+ ...(extras || {})
154
+ });
155
+ request.identity = identity;
156
+ return identity;
157
+ }
158
+ /** Initial `{ outer }` fragment for `startRecord` (Activix merges completion into the same `outer`). */
159
+ function buildActivixStartIo(aiRequestId, activityType, requestData) {
160
+ const input = requestData !== undefined &&
161
+ typeof requestData === 'object' &&
162
+ !Array.isArray(requestData) &&
163
+ Object.keys(requestData).length > 0
164
+ ? { activityType, request: requestData }
165
+ : { activityType };
166
+ return activixActivityIo(activixOuterTier(input, null, { aiRequestId }));
167
+ }
168
+ /**
169
+ * Manages activity tracking lifecycle
170
+ */
171
+ export class ActivityManager {
172
+ activix;
173
+ initPromise;
174
+ mainCollectionName;
175
+ skillExecutionsCollectionName = 'skill-executions';
176
+ badRequestsCollectionName; // Bad requests collection name
177
+ logger;
178
+ constructor(config) {
179
+ this.logger = config.logger;
180
+ if (config.enableActivityTracking) {
181
+ const { collectionName, badRequestsCollectionName } = resolveActivityTrackingConfig();
182
+ this.mainCollectionName = collectionName;
183
+ this.badRequestsCollectionName = badRequestsCollectionName;
184
+ try {
185
+ // Map gateway statuses to Activix statusValues so lifecycle semantics stay compatible.
186
+ const statusValues = {
187
+ started: 'started',
188
+ inProgress: 'in_progress',
189
+ completed: 'success',
190
+ failed: 'failed',
191
+ timeout: 'timeout'
192
+ };
193
+ this.activix = config.customTracker ?? new Activix({
194
+ // Keep mode explicit for operational clarity (matches integration checklist expectations).
195
+ storageMode: 'automatic',
196
+ collections: [
197
+ {
198
+ name: collectionName,
199
+ statusValues,
200
+ primaryKey: 'activityId',
201
+ primaryKeyPrefix: 'act-'
202
+ },
203
+ {
204
+ name: this.skillExecutionsCollectionName,
205
+ statusValues,
206
+ primaryKey: 'activityId',
207
+ primaryKeyPrefix: 'act-'
208
+ },
209
+ {
210
+ name: badRequestsCollectionName,
211
+ statusValues,
212
+ primaryKey: 'activityId',
213
+ primaryKeyPrefix: 'act-'
214
+ }
215
+ ],
216
+ defaultCollection: collectionName,
217
+ diagnostics: {
218
+ owner: '@x12i/ai-gateway',
219
+ component: 'activity-manager',
220
+ instanceLabel: 'default',
221
+ workerId: String(process.pid)
222
+ },
223
+ logger: this.logger,
224
+ errorHandling: {
225
+ onConnectionError: 'silent',
226
+ onPersistError: 'silent',
227
+ retry: {
228
+ maxRetries: 0,
229
+ retryDelay: 0,
230
+ exponentialBackoff: false
231
+ }
232
+ }
233
+ });
234
+ this.initPromise = this.activix.init().catch((error) => {
235
+ // MongoDB config not available - log warning but don't throw.
236
+ // This allows tests and development to work without MongoDB.
237
+ this.logger.warn('Activity tracking enabled but MongoDB configuration not available. Activity records will not be persisted.', {
238
+ error: error instanceof Error ? error.message : String(error),
239
+ hint: 'Set MONGO_URI and MONGO_LOGS_DB (or MONGO_DB) environment variables to enable activity tracking persistence'
240
+ });
241
+ this.activix = undefined;
242
+ });
243
+ this.logger.debug('Activity tracking enabled with Activix', {
244
+ collection: collectionName,
245
+ badRequestsCollection: badRequestsCollectionName,
246
+ skillExecutionsCollection: this.skillExecutionsCollectionName
247
+ });
248
+ }
249
+ catch (error) {
250
+ this.logger.warn('Failed to initialize activity tracking (Activix). Activity records will not be persisted.', {
251
+ error: error instanceof Error ? error.message : String(error)
252
+ });
253
+ this.activix = undefined;
254
+ }
255
+ }
256
+ }
257
+ /**
258
+ * Gets upstream-provided AI request ID
259
+ *
260
+ * @param request - Enhanced LLM request
261
+ * @returns AI request ID string
262
+ */
263
+ generateJobId(request) {
264
+ return readAiRequestIdFromRequest(request);
265
+ }
266
+ /**
267
+ * Starts activity tracking for a request
268
+ *
269
+ * IMPORTANT: This method ONLY sends REQUEST data (no response).
270
+ * The same record will be updated later via logSuccess() or logFailure()
271
+ * with response/timing data.
272
+ *
273
+ * KEY CONCEPT: jobId is NOT the unique identifier!
274
+ * - jobId = entire job (can have 100+ activities sharing the same jobId)
275
+ * - taskId = task within a job (can have multiple activities)
276
+ * - Each activity gets its own unique identifier (_id/recordId) from ActivityTracker
277
+ * - The returned ActivityMetadata contains this unique identifier for later updates
278
+ *
279
+ * This ensures:
280
+ * - On start: Only request data is stored → creates NEW record with unique _id
281
+ * - On finish: Only response data is added → updates SAME record by _id/recordId
282
+ * - No duplication: Request data sent once, response data sent once
283
+ * - Multiple activities per job: Each activity is a separate record, grouped by jobId
284
+ *
285
+ * @param request - Enhanced LLM request
286
+ * @param startTime - Request start timestamp
287
+ * @returns Activity metadata (contains unique _id/recordId) or undefined if tracking is disabled
288
+ */
289
+ async startActivity(request, startTime) {
290
+ const identity = ensureGatewayRequestIdentity(request);
291
+ const aiRequestId = identity.aiRequestId;
292
+ // Capture ONLY request data - NO response data at this stage
293
+ // Note: v2.3.2+ excludes flat request/config fields from root level
294
+ // Fields like messages, instructions, prompt, input, context, workingMemory,
295
+ // model, provider, temperature, maxTokens should only be in request/config objects
296
+ //
297
+ // `runContext` mirrors `request.identity` from ensureGatewayRequestIdentity (sessionId, instance, graph/skill linkage,
298
+ // gateway context fields, and pass-through of upstream custom keys) for Activix v5 persistence.
299
+ if (!this.activix) {
300
+ return undefined;
301
+ }
302
+ if (this.initPromise) {
303
+ await this.initPromise;
304
+ }
305
+ if (!this.activix) {
306
+ return undefined;
307
+ }
308
+ const activityMetadata = {
309
+ aiRequestId,
310
+ jobTypeId: request.jobTypeId,
311
+ agentId: request.agentId,
312
+ taskId: request.taskId,
313
+ taskTypeId: request.taskTypeId,
314
+ startTime,
315
+ status: 'started',
316
+ activityType: 'gateway-invocation',
317
+ // Activix v5+: correlation BSON field is `runContext` (same object as `request.identity`)
318
+ runContext: identity
319
+ // Removed root-level fields per v2.3.2:
320
+ // - instructions, prompt, input, context, messages, workingMemory → only in request object
321
+ // - provider, model, temperature, maxTokens → only in config object
322
+ // - NO response, endTime, duration (these are added via logSuccess)
323
+ };
324
+ // Config snapshot
325
+ // CRITICAL: This captures the exact config sent to the router (finalRouterConfig)
326
+ // This ensures activity records show the actual config that was sent, including that response_format was removed
327
+ if (request.config !== undefined) {
328
+ // Verify response_format is not present (for debugging)
329
+ const hasResponseFormat = 'responseFormat' in request.config || 'response_format' in request.config;
330
+ if (hasResponseFormat) {
331
+ this.logger.warn('Activity tracking received config with response_format - this should not happen', {
332
+ aiRequestId,
333
+ hasResponseFormat: 'responseFormat' in request.config,
334
+ hasResponse_format: 'response_format' in request.config,
335
+ configKeys: Object.keys(request.config)
336
+ });
337
+ }
338
+ activityMetadata.config = {
339
+ model: request.config.model,
340
+ provider: request.config.provider || null, // Ensure provider is captured (may be null if not set)
341
+ temperature: request.config.temperature,
342
+ maxTokens: request.config.maxTokens,
343
+ rawConfig: request.config // ✅ Captures finalRouterConfig (exact config sent to router)
344
+ };
345
+ this.logger.debug('Activity tracking config captured', {
346
+ aiRequestId,
347
+ model: request.config.model,
348
+ provider: request.config.provider,
349
+ hasResponseFormat: hasResponseFormat,
350
+ rawConfigKeys: Object.keys(request.config).slice(0, 10)
351
+ });
352
+ }
353
+ // Build request object snapshots (raw = incoming; parsed = constructed messages/meta)
354
+ const rawSnapshot = request._rawRequest ?? {
355
+ instructions: request.instructions,
356
+ context: request.context,
357
+ prompt: request.prompt,
358
+ messages: request.messages,
359
+ workingMemory: request.workingMemory,
360
+ config: request.config
361
+ };
362
+ const parsedSnapshot = request._parsedRequest ?? {};
363
+ const requestData = {};
364
+ // raw snapshot (only allowed fields)
365
+ if (rawSnapshot.instructions !== undefined ||
366
+ rawSnapshot.context !== undefined ||
367
+ rawSnapshot.prompt !== undefined) {
368
+ requestData.raw = {
369
+ instructions: rawSnapshot.instructions,
370
+ context: rawSnapshot.context,
371
+ prompt: rawSnapshot.prompt
372
+ };
373
+ }
374
+ // parsed snapshot (only allowed fields)
375
+ // Ensure parsed is populated if parsedSnapshot has data, even if individual fields are undefined
376
+ if (parsedSnapshot.instructions !== undefined ||
377
+ parsedSnapshot.context !== undefined ||
378
+ parsedSnapshot.prompt !== undefined) {
379
+ requestData.parsed = {
380
+ instructions: parsedSnapshot.instructions,
381
+ context: parsedSnapshot.context,
382
+ prompt: parsedSnapshot.prompt
383
+ };
384
+ }
385
+ else if (Object.keys(parsedSnapshot).length > 0) {
386
+ // If parsedSnapshot exists but doesn't have instructions/context/prompt,
387
+ // still create parsed with what's available (mirror of raw request after processing)
388
+ requestData.parsed = {
389
+ instructions: rawSnapshot.instructions,
390
+ context: rawSnapshot.context,
391
+ prompt: rawSnapshot.prompt
392
+ };
393
+ }
394
+ // DEBUG: Include format guidance (prefix + schema) for objectTypes debugging
395
+ // This helps verify what was fetched and appended to instructions
396
+ if (parsedSnapshot._formatGuidance) {
397
+ requestData.formatGuidance = {
398
+ prefixKey: parsedSnapshot._formatGuidance.prefixKey,
399
+ prefixContent: parsedSnapshot._formatGuidance.prefixContent,
400
+ schemaGuidance: parsedSnapshot._formatGuidance.schemaGuidance,
401
+ hasMultipleObjectTypes: parsedSnapshot._formatGuidance.hasMultipleObjectTypes
402
+ };
403
+ }
404
+ // Include output format information from validation
405
+ if (parsedSnapshot._outputFormat) {
406
+ activityMetadata.outputFormat = {
407
+ spec: parsedSnapshot._outputFormat.spec,
408
+ level: parsedSnapshot._outputFormat.level,
409
+ validated: parsedSnapshot._outputFormat.validated
410
+ };
411
+ }
412
+ // Always use flex-md parsing - no object type processing needed
413
+ // other allowed fields on request
414
+ // Ensure instructions is included if not already in raw/parsed
415
+ if (requestData.raw?.instructions === undefined &&
416
+ requestData.parsed?.instructions === undefined &&
417
+ rawSnapshot.instructions !== undefined) {
418
+ // If no raw/parsed exists yet, create raw with instructions
419
+ if (!requestData.raw) {
420
+ requestData.raw = { instructions: rawSnapshot.instructions };
421
+ }
422
+ else {
423
+ requestData.raw.instructions = rawSnapshot.instructions;
424
+ }
425
+ }
426
+ // Input field has been removed - use workingMemory.input instead
427
+ // Store messages array (actual messages sent to LLM provider)
428
+ const messagesToStore = parsedSnapshot.messages ?? rawSnapshot.messages;
429
+ if (messagesToStore !== undefined) {
430
+ requestData.messages = messagesToStore;
431
+ }
432
+ if (rawSnapshot.workingMemory !== undefined) {
433
+ requestData.workingMemory = rawSnapshot.workingMemory;
434
+ }
435
+ // Only attach if any field is present
436
+ const hasRequest = (requestData.raw &&
437
+ (requestData.raw.instructions !== undefined ||
438
+ requestData.raw.context !== undefined ||
439
+ requestData.raw.prompt !== undefined)) ||
440
+ (requestData.parsed &&
441
+ (requestData.parsed.instructions !== undefined ||
442
+ requestData.parsed.context !== undefined ||
443
+ requestData.parsed.prompt !== undefined)) ||
444
+ requestData.messages !== undefined ||
445
+ requestData.workingMemory !== undefined ||
446
+ requestData.formatGuidance !== undefined;
447
+ if (hasRequest) {
448
+ activityMetadata.request = requestData;
449
+ }
450
+ Object.assign(activityMetadata, buildActivixStartIo(aiRequestId, 'gateway-invocation', activityMetadata.request));
451
+ // Start activity with error handling
452
+ // Note: If startActivity fails (e.g., tracker not initialized, duplicate key on jobId),
453
+ // we still return undefined to allow the request to proceed without blocking
454
+ try {
455
+ const startedResult = await this.activix.startRecord(activityMetadata, {
456
+ collection: this.mainCollectionName
457
+ });
458
+ const started = {
459
+ ...startedResult.record,
460
+ activityId: startedResult.activityId,
461
+ recordId: startedResult.recordId
462
+ };
463
+ this.logger.info('Activity tracking start recorded', {
464
+ aiRequestId,
465
+ activityId: started?.activityId,
466
+ collection: this.mainCollectionName
467
+ });
468
+ return started;
469
+ }
470
+ catch (error) {
471
+ this.logger.error('Failed to start activity tracking', {
472
+ aiRequestId,
473
+ error: error instanceof Error ? error.message : String(error),
474
+ errorName: error instanceof Error ? error.name : 'UnknownError',
475
+ errorStack: error instanceof Error ? error.stack : undefined,
476
+ // Note: If you see "duplicate key error on jobId", this indicates the database
477
+ // has a unique index on jobId, which is INCORRECT. jobId should NOT be unique
478
+ // as multiple activities can share the same jobId. The unique identifier should
479
+ // be _id (MongoDB ObjectId), not jobId.
480
+ note: 'Activity tracking failed but request will continue'
481
+ });
482
+ // Return undefined to allow request to proceed without activity tracking
483
+ return undefined;
484
+ }
485
+ }
486
+ /**
487
+ * Starts skill execution activity tracking
488
+ *
489
+ * Similar to startActivity() but creates a skill-execution activity record instead of gateway-invocation.
490
+ * Skill executions are tracked separately and stored in the 'skill-executions' collection.
491
+ *
492
+ * @param request - Enhanced LLM request (must be a skill execution)
493
+ * @param instructionMetadata - Optional audit fields when caller tracks instruction catalog info
494
+ * @param startTime - Request start timestamp
495
+ * @returns Activity metadata (contains unique _id/recordId) or undefined if tracking is disabled
496
+ */
497
+ async startSkillExecution(request, instructionMetadata = {}, startTime) {
498
+ const aiRequest = request;
499
+ const skillId = aiRequest.skillId;
500
+ const skillKey = skillId ?? 'inline-template';
501
+ if (skillId) {
502
+ aiRequest.skillId = skillId;
503
+ }
504
+ // Determine inference type (will be extracted in gateway.ts and passed via request metadata)
505
+ const inferenceType = aiRequest.inferenceType || 'question-answer';
506
+ const identity = ensureGatewayRequestIdentity(request);
507
+ const aiRequestId = identity.aiRequestId;
508
+ if (!this.activix) {
509
+ return undefined;
510
+ }
511
+ if (this.initPromise) {
512
+ await this.initPromise;
513
+ }
514
+ if (!this.activix) {
515
+ return undefined;
516
+ }
517
+ // Build activity metadata for skill execution
518
+ const activityMetadata = {
519
+ activityType: 'skill-execution', // ✅ Required: skill-execution type
520
+ skillKey,
521
+ skillId: skillId ?? skillKey,
522
+ inferenceType, // ✅ Recommended: type of inference
523
+ aiRequestId,
524
+ jobTypeId: request.jobTypeId,
525
+ agentId: request.agentId,
526
+ taskId: request.taskId,
527
+ taskTypeId: request.taskTypeId,
528
+ startTime,
529
+ status: 'started',
530
+ runContext: identity,
531
+ ...(instructionMetadata.key && { instructionKey: instructionMetadata.key }),
532
+ ...(instructionMetadata.version && { instructionVersion: instructionMetadata.version }),
533
+ ...(instructionMetadata.rawContent && { instructionRawContent: instructionMetadata.rawContent }),
534
+ // ✅ Parent-child skill relationships
535
+ ...(aiRequest.masterSkillActivityId && { masterSkillActivityId: aiRequest.masterSkillActivityId }),
536
+ ...(aiRequest.masterSkillId && { masterSkillId: aiRequest.masterSkillId })
537
+ };
538
+ // Config snapshot (same as startActivity)
539
+ if (request.config !== undefined) {
540
+ activityMetadata.config = {
541
+ model: request.config.model,
542
+ provider: request.config.provider || null,
543
+ temperature: request.config.temperature,
544
+ maxTokens: request.config.maxTokens,
545
+ rawConfig: request.config
546
+ };
547
+ }
548
+ // Build request object snapshots (same as startActivity)
549
+ const rawSnapshot = request._rawRequest ?? {
550
+ instructions: request.instructions,
551
+ context: request.context,
552
+ prompt: request.prompt,
553
+ messages: request.messages,
554
+ workingMemory: request.workingMemory,
555
+ config: request.config
556
+ };
557
+ const parsedSnapshot = request._parsedRequest ?? {};
558
+ const requestData = {};
559
+ // raw snapshot
560
+ if (rawSnapshot.instructions !== undefined ||
561
+ rawSnapshot.context !== undefined ||
562
+ rawSnapshot.prompt !== undefined) {
563
+ requestData.raw = {
564
+ instructions: rawSnapshot.instructions,
565
+ context: rawSnapshot.context,
566
+ prompt: rawSnapshot.prompt
567
+ };
568
+ }
569
+ // parsed snapshot
570
+ if (parsedSnapshot.instructions !== undefined ||
571
+ parsedSnapshot.context !== undefined ||
572
+ parsedSnapshot.prompt !== undefined) {
573
+ requestData.parsed = {
574
+ instructions: parsedSnapshot.instructions,
575
+ context: parsedSnapshot.context,
576
+ prompt: parsedSnapshot.prompt
577
+ };
578
+ }
579
+ // Include skill-specific request structure
580
+ if (rawSnapshot.input !== undefined) {
581
+ // Parse input from JSON string to object if it's a string
582
+ let parsedInput = rawSnapshot.input;
583
+ if (typeof parsedInput === 'string') {
584
+ try {
585
+ parsedInput = JSON.parse(parsedInput);
586
+ }
587
+ catch (e) {
588
+ // If parsing fails, use the original string
589
+ // This handles cases where input is a plain string, not JSON
590
+ }
591
+ }
592
+ requestData.input = parsedInput;
593
+ }
594
+ // Store messages array (actual messages sent to LLM provider)
595
+ const messagesToStore = parsedSnapshot.messages ?? rawSnapshot.messages;
596
+ if (messagesToStore !== undefined) {
597
+ requestData.messages = messagesToStore;
598
+ }
599
+ if (rawSnapshot.workingMemory !== undefined) {
600
+ requestData.workingMemory = rawSnapshot.workingMemory;
601
+ }
602
+ // Add skill-specific request structure
603
+ if (rawSnapshot.workingMemory || rawSnapshot.context) {
604
+ requestData.skill = {
605
+ variables: rawSnapshot.workingMemory,
606
+ context: rawSnapshot.context
607
+ };
608
+ }
609
+ // Only attach if any field is present
610
+ const hasRequest = (requestData.raw &&
611
+ (requestData.raw.instructions !== undefined ||
612
+ requestData.raw.context !== undefined ||
613
+ requestData.raw.prompt !== undefined)) ||
614
+ (requestData.parsed &&
615
+ (requestData.parsed.instructions !== undefined ||
616
+ requestData.parsed.context !== undefined ||
617
+ requestData.parsed.prompt !== undefined)) ||
618
+ requestData.messages !== undefined ||
619
+ requestData.workingMemory !== undefined ||
620
+ requestData.skill !== undefined;
621
+ if (hasRequest) {
622
+ activityMetadata.request = requestData;
623
+ }
624
+ Object.assign(activityMetadata, buildActivixStartIo(aiRequestId, 'skill-execution', activityMetadata.request));
625
+ // Start skill execution activity with error handling
626
+ // Activix requires explicit collection selection.
627
+ try {
628
+ const startedResult = await this.activix.startRecord(activityMetadata, {
629
+ collection: this.skillExecutionsCollectionName
630
+ });
631
+ const started = {
632
+ ...startedResult.record,
633
+ activityId: startedResult.activityId,
634
+ recordId: startedResult.recordId
635
+ };
636
+ this.logger.info('Skill execution activity tracking start recorded', {
637
+ aiRequestId,
638
+ skillId,
639
+ activityId: started?.activityId,
640
+ collection: 'skill-executions',
641
+ hasMasterSkillActivityId: !!aiRequest.masterSkillActivityId,
642
+ hasMasterSkillId: !!aiRequest.masterSkillId
643
+ });
644
+ return started;
645
+ }
646
+ catch (error) {
647
+ this.logger.error('Failed to start skill execution activity tracking', {
648
+ aiRequestId,
649
+ skillId,
650
+ error: error instanceof Error ? error.message : String(error),
651
+ errorName: error instanceof Error ? error.name : 'UnknownError',
652
+ errorStack: error instanceof Error ? error.stack : undefined,
653
+ note: 'Skill execution activity tracking failed but request will continue'
654
+ });
655
+ return undefined;
656
+ }
657
+ }
658
+ /**
659
+ * Logs successful activity completion
660
+ *
661
+ * IMPORTANT: This method ONLY sends RESPONSE/TIMING data (no request data).
662
+ * It updates the SAME record created by startActivity() using the unique record identifier.
663
+ *
664
+ * KEY CONCEPT: jobId is NOT the unique identifier!
665
+ * - jobId = entire job (can have 100+ activities sharing the same jobId)
666
+ * - taskId = task within a job (can have multiple activities)
667
+ * - Each activity has its own unique identifier (_id/recordId) returned by startActivity()
668
+ *
669
+ * This ensures:
670
+ * - On start: Only request data is stored (via startActivity) → creates new record with unique _id
671
+ * - On finish: Only response data is added (via logSuccess) → updates same record by _id/recordId
672
+ * - No duplication: Request data sent once, response data sent once
673
+ * - Same record: Updated by unique identifier (_id/recordId), NOT by jobId
674
+ *
675
+ * Structure sent to ActivityTracker:
676
+ * - response: { content, metadata } (response object)
677
+ * - endTime, duration (timing completion)
678
+ * - cost (cost calculation)
679
+ * - status: 'success' (updated by ActivityTracker)
680
+ * - NO request data (already stored in startActivity)
681
+ * - NO config data (already stored in startActivity)
682
+ *
683
+ * @param activity - Activity metadata from startActivity() (contains unique _id/recordId for record lookup)
684
+ * @param details - Success details (cost, response, timing)
685
+ */
686
+ async logSuccess(activity, details) {
687
+ if (!this.activix || !activity) {
688
+ return;
689
+ }
690
+ if (this.initPromise) {
691
+ await this.initPromise;
692
+ }
693
+ if (!this.activix) {
694
+ return;
695
+ }
696
+ const collection = activity.activityType === 'skill-execution'
697
+ ? this.skillExecutionsCollectionName
698
+ : activity.activityType === 'bad-request'
699
+ ? this.badRequestsCollectionName ?? this.mainCollectionName
700
+ : this.mainCollectionName;
701
+ // Enhanced logging to diagnose activity update issues
702
+ const activityId = activity.activityId || 'MISSING';
703
+ this.logger.info('Activity tracking updating (success)', {
704
+ aiRequestId: activity.aiRequestId,
705
+ activityId: activityId,
706
+ hasActivityId: !!activity.activityId,
707
+ endTime: details.endTime,
708
+ duration: details.duration,
709
+ activityKeys: Object.keys(activity).join(', ')
710
+ });
711
+ // Only send response/timing data - ActivityTracker will update the same record
712
+ // identified by the unique identifier (_id/recordId) in the activity object.
713
+ // jobId is just metadata for grouping - multiple activities can share the same jobId.
714
+ // Request data is NOT re-sent here.
715
+ // Log the full response structure being sent
716
+ this.logger.debug('Calling ActivityTracker.logSuccess', {
717
+ aiRequestId: activity.aiRequestId,
718
+ activityId: activityId,
719
+ cost: details.cost,
720
+ hasResponse: !!details.response,
721
+ responseStructure: {
722
+ hasRaw: !!details.response?.raw,
723
+ hasParsed: !!details.response?.parsed,
724
+ hasMetadata: !!details.response?.metadata,
725
+ hasContent: !!details.response?.content,
726
+ rawContentPreview: details.response?.raw?.content?.substring(0, 100),
727
+ parsedContentExists: !!details.response?.parsed?.parsedContent,
728
+ metadataKeys: details.response?.metadata ? Object.keys(details.response.metadata).slice(0, 10) : []
729
+ },
730
+ fullResponseKeys: details.response ? Object.keys(details.response) : [],
731
+ endTime: details.endTime,
732
+ duration: details.duration
733
+ });
734
+ try {
735
+ if (!activity.activityId || !collection) {
736
+ this.logger.warn('Skipping activity completion: missing activityId/collection', {
737
+ aiRequestId: activity.aiRequestId,
738
+ activityId: activityId,
739
+ activityType: activity.activityType,
740
+ collection
741
+ });
742
+ return;
743
+ }
744
+ await this.activix.completeRecord(activity.activityId, {
745
+ cost: details.cost,
746
+ response: details.response,
747
+ outer: {
748
+ output: details.response,
749
+ metadata: {}
750
+ },
751
+ endTime: details.endTime,
752
+ duration: details.duration
753
+ }, { collection });
754
+ this.logger.debug('Activix.completeRecord completed', {
755
+ aiRequestId: activity.aiRequestId,
756
+ activityId: activityId
757
+ });
758
+ }
759
+ catch (error) {
760
+ // Note: With silent error handling, Activix may not throw, but if it does, we want to log it.
761
+ this.logger.error('Failed to log success activity (non-blocking)', {
762
+ aiRequestId: activity.aiRequestId,
763
+ activityId: activityId,
764
+ error: error instanceof Error ? error.message : String(error),
765
+ errorName: error instanceof Error ? error.name : 'UnknownError',
766
+ errorStack: error instanceof Error ? error.stack : undefined
767
+ });
768
+ }
769
+ }
770
+ /**
771
+ * Logs failed activity
772
+ *
773
+ * IMPORTANT: This method ONLY sends ERROR/TIMING data (no request/response data).
774
+ * It updates the SAME record created by startActivity() using the unique record identifier.
775
+ *
776
+ * KEY CONCEPT: jobId is NOT the unique identifier!
777
+ * - jobId = entire job (can have 100+ activities sharing the same jobId)
778
+ * - taskId = task within a job (can have multiple activities)
779
+ * - Each activity has its own unique identifier (_id/recordId) returned by startActivity()
780
+ *
781
+ * Structure sent to ActivityTracker:
782
+ * - error (error message)
783
+ * - endTime, duration (timing completion)
784
+ * - status: 'failed' (updated by ActivityTracker)
785
+ * - NO request data (already stored in startActivity)
786
+ * - NO response data (request failed)
787
+ *
788
+ * @param activity - Activity metadata from startActivity() (contains unique _id/recordId for record lookup)
789
+ * @param error - Error that occurred
790
+ * @param details - Failure details (timing, error message)
791
+ */
792
+ async logFailure(activity, error, details) {
793
+ if (!this.activix || !activity) {
794
+ return;
795
+ }
796
+ if (this.initPromise) {
797
+ await this.initPromise;
798
+ }
799
+ if (!this.activix) {
800
+ return;
801
+ }
802
+ const collection = activity.activityType === 'skill-execution'
803
+ ? this.skillExecutionsCollectionName
804
+ : activity.activityType === 'bad-request'
805
+ ? this.badRequestsCollectionName ?? this.mainCollectionName
806
+ : this.mainCollectionName;
807
+ this.logger.info('Activity tracking updating (failure)', {
808
+ aiRequestId: activity.aiRequestId,
809
+ activityId: activity.activityId,
810
+ endTime: details.endTime,
811
+ duration: details.duration,
812
+ error: details.error,
813
+ failureType: details.failureType,
814
+ failureSubtype: details.failureSubtype,
815
+ hasResponse: !!details.response
816
+ });
817
+ // Only send error/timing data - ActivityTracker will update the same record
818
+ // identified by the unique identifier (_id/recordId) in the activity object.
819
+ // jobId is just metadata for grouping - multiple activities can share the same jobId.
820
+ // Request data is NOT re-sent here.
821
+ // Include failureType and failureSubtype in error details for activity tracking
822
+ // For response-parsing-failure, also include response data so we can see what failed
823
+ const errorDetails = {
824
+ endTime: details.endTime,
825
+ duration: details.duration,
826
+ error: details.error
827
+ };
828
+ if (details.failureType) {
829
+ errorDetails.failureType = details.failureType;
830
+ }
831
+ if (details.failureSubtype) {
832
+ errorDetails.failureSubtype = details.failureSubtype;
833
+ }
834
+ // For response-parsing-failure, include response data so we can see what failed
835
+ if (details.failureType === 'response-parsing-failure' && details.response) {
836
+ errorDetails.response = details.response;
837
+ errorDetails.outer = {
838
+ output: details.response,
839
+ metadata: {}
840
+ };
841
+ this.logger.debug('Including response data in failure log (response-parsing-failure)', {
842
+ aiRequestId: activity.aiRequestId,
843
+ hasRawContent: !!details.response.raw?.content,
844
+ hasParsedContent: !!details.response.parsed?.content
845
+ });
846
+ }
847
+ try {
848
+ if (!activity.activityId || !collection) {
849
+ this.logger.warn('Skipping activity failure: missing activityId/collection', {
850
+ aiRequestId: activity.aiRequestId,
851
+ activityId: activity.activityId,
852
+ activityType: activity.activityType,
853
+ collection
854
+ });
855
+ return;
856
+ }
857
+ await this.activix.failRecord(activity.activityId, error, errorDetails, { collection, upsertIfMissing: false });
858
+ this.logger.debug('Activix.failRecord completed', {
859
+ aiRequestId: activity.aiRequestId,
860
+ activityId: activity.activityId
861
+ });
862
+ }
863
+ catch (trackingError) {
864
+ // Log error so we can see when ActivityTracker.logFailure fails
865
+ // Note: With silent error handling, ActivityTracker may not throw, but if it does, we want to log it
866
+ this.logger.error('Failed to log failure activity (non-blocking)', {
867
+ aiRequestId: activity.aiRequestId,
868
+ activityId: activity.activityId,
869
+ error: trackingError instanceof Error ? trackingError.message : String(trackingError),
870
+ errorName: trackingError instanceof Error ? trackingError.name : 'UnknownError',
871
+ errorStack: trackingError instanceof Error ? trackingError.stack : undefined,
872
+ originalError: error.message
873
+ });
874
+ }
875
+ }
876
+ /**
877
+ * Logs a bad request (error that occurred before startActivity)
878
+ *
879
+ * This method creates a new activity record specifically for tracking validation
880
+ * errors, format extraction failures, and other errors that occur before the
881
+ * normal activity lifecycle begins.
882
+ *
883
+ * Uses native ActivityTracker support for bad requests collection via the
884
+ * `collectionName` option in `startActivity()` and `logFailure()` methods.
885
+ * The bad requests collection is configured via `badRequestsCollectionName` in
886
+ * the ActivityTracker constructor (defaults to 'ai-bad-requests').
887
+ *
888
+ * Bad requests are automatically routed to the separate bad requests collection
889
+ * and can be filtered by `failureType` field:
890
+ * - 'validation-failure': Request validation errors
891
+ * - 'format-extraction-failure': Format extraction errors
892
+ * - 'configuration-failure': Configuration errors
893
+ *
894
+ * @param request - The request that failed
895
+ * @param error - The error that occurred
896
+ * @param details - Additional error details
897
+ * @param startTime - When the request started
898
+ */
899
+ async logBadRequest(request, error, details, startTime) {
900
+ const identity = ensureGatewayRequestIdentity(request);
901
+ const aiRequestId = identity.aiRequestId;
902
+ const endTime = details.endTime;
903
+ const duration = details.duration;
904
+ if (!this.activix) {
905
+ return;
906
+ }
907
+ if (this.initPromise) {
908
+ await this.initPromise;
909
+ }
910
+ if (!this.activix) {
911
+ return;
912
+ }
913
+ this.logger.info('Logging bad request to activities', {
914
+ aiRequestId,
915
+ error: details.error,
916
+ errorType: details.errorType,
917
+ failureType: details.failureType,
918
+ hasDiagnosticInfo: !!details.diagnosticInfo
919
+ });
920
+ // Create a minimal activity metadata for bad requests
921
+ // Start with 'started' status, then logFailure will update it to 'failed'
922
+ const badRequestMetadata = {
923
+ activityType: 'bad-request',
924
+ aiRequestId,
925
+ jobTypeId: request.jobTypeId,
926
+ agentId: request.agentId,
927
+ taskId: request.taskId,
928
+ taskTypeId: request.taskTypeId,
929
+ startTime,
930
+ status: 'started',
931
+ runContext: identity
932
+ };
933
+ // Include request data if available
934
+ const requestData = {};
935
+ if (request.instructions !== undefined) {
936
+ requestData.raw = {
937
+ instructions: typeof request.instructions === 'string'
938
+ ? request.instructions.substring(0, 2000) // Limit size
939
+ : request.instructions
940
+ };
941
+ }
942
+ // Input field has been removed - use workingMemory.input instead
943
+ if (request.workingMemory !== undefined && request.workingMemory !== null) {
944
+ requestData.workingMemory = request.workingMemory;
945
+ }
946
+ if (Object.keys(requestData).length > 0) {
947
+ badRequestMetadata.request = requestData;
948
+ }
949
+ // Include config if available
950
+ if (request.config !== undefined) {
951
+ badRequestMetadata.config = {
952
+ model: request.config.model,
953
+ provider: request.config.provider,
954
+ temperature: request.config.temperature,
955
+ maxTokens: request.config.maxTokens,
956
+ rawConfig: request.config
957
+ };
958
+ }
959
+ // Add error details
960
+ const errorDetails = {
961
+ endTime,
962
+ duration,
963
+ error: details.error
964
+ };
965
+ if (details.errorType) {
966
+ errorDetails.errorType = details.errorType;
967
+ }
968
+ if (details.failureType) {
969
+ errorDetails.failureType = details.failureType;
970
+ }
971
+ // Include diagnostic info if provided
972
+ if (details.diagnosticInfo) {
973
+ errorDetails.diagnosticInfo = details.diagnosticInfo;
974
+ }
975
+ Object.assign(badRequestMetadata, buildActivixStartIo(aiRequestId, 'bad-request', badRequestMetadata.request));
976
+ try {
977
+ const collection = this.badRequestsCollectionName ?? this.mainCollectionName;
978
+ if (!collection) {
979
+ this.logger.warn('Skipping bad request tracking: missing bad requests collection');
980
+ return;
981
+ }
982
+ const startedResult = await this.activix.startRecord(badRequestMetadata, { collection });
983
+ const activity = {
984
+ ...startedResult.record,
985
+ activityId: startedResult.activityId,
986
+ recordId: startedResult.recordId
987
+ };
988
+ if (!activity.activityId) {
989
+ this.logger.warn('Skipping bad request failure tracking: missing activityId', { aiRequestId });
990
+ return;
991
+ }
992
+ await this.activix.failRecord(activity.activityId, error, errorDetails, {
993
+ collection,
994
+ upsertIfMissing: false
995
+ });
996
+ this.logger.debug('Bad request logged to activities', {
997
+ aiRequestId,
998
+ activityId: activity.activityId,
999
+ collection
1000
+ });
1001
+ }
1002
+ catch (trackingError) {
1003
+ // Don't fail if activity tracking fails for bad requests
1004
+ this.logger.error('Failed to log bad request to activities (non-blocking)', {
1005
+ aiRequestId,
1006
+ error: trackingError instanceof Error ? trackingError.message : String(trackingError),
1007
+ errorName: trackingError instanceof Error ? trackingError.name : 'UnknownError',
1008
+ errorStack: trackingError instanceof Error ? trackingError.stack : undefined,
1009
+ badRequestsCollectionName: this.badRequestsCollectionName
1010
+ });
1011
+ }
1012
+ }
1013
+ /**
1014
+ * Gets the underlying activity tracker instance
1015
+ *
1016
+ * @returns Activix instance or undefined if not enabled
1017
+ */
1018
+ getTracker() {
1019
+ return this.activix;
1020
+ }
1021
+ /**
1022
+ * Get status of activity tracker
1023
+ */
1024
+ getStatus() {
1025
+ return {
1026
+ activityTracking: {
1027
+ enabled: !!this.activix,
1028
+ tracker: this.activix ? { type: 'Activix' } : undefined
1029
+ }
1030
+ };
1031
+ }
1032
+ /**
1033
+ * Shutdown tracker
1034
+ */
1035
+ async shutdown() {
1036
+ this.logger.info('Shutting down ActivityManager');
1037
+ const ax = this.activix;
1038
+ this.activix = undefined;
1039
+ this.initPromise = undefined;
1040
+ if (ax) {
1041
+ try {
1042
+ await ax.close();
1043
+ }
1044
+ catch (error) {
1045
+ this.logger.warn('ActivityManager shutdown: Activix.close failed (non-blocking)', {
1046
+ error: error instanceof Error ? error.message : String(error)
1047
+ });
1048
+ }
1049
+ }
1050
+ }
1051
+ }