@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.
- package/README.md +4259 -0
- package/config.defaults.json +31 -0
- package/dist/activity-manager.d.ts +206 -0
- package/dist/activity-manager.js +1051 -0
- package/dist/config/activity-tracking-config.d.ts +11 -0
- package/dist/config/activity-tracking-config.js +15 -0
- package/dist/config.defaults.json +31 -0
- package/dist/content-normalizer/content-normalizer.d.ts +46 -0
- package/dist/content-normalizer/content-normalizer.js +393 -0
- package/dist/content-normalizer/index.d.ts +7 -0
- package/dist/content-normalizer/index.js +6 -0
- package/dist/content-normalizer/types.d.ts +33 -0
- package/dist/content-normalizer/types.js +4 -0
- package/dist/defaults/instructions-blocks.json +61 -0
- package/dist/defaults/model-config.json +16 -0
- package/dist/defaults/template-rendering.json +6 -0
- package/dist/flex-md-loader.d.ts +109 -0
- package/dist/flex-md-loader.js +940 -0
- package/dist/gateway-config.d.ts +49 -0
- package/dist/gateway-config.js +292 -0
- package/dist/gateway-conversion.d.ts +29 -0
- package/dist/gateway-conversion.js +174 -0
- package/dist/gateway-instructions.d.ts +30 -0
- package/dist/gateway-instructions.js +62 -0
- package/dist/gateway-memory.d.ts +51 -0
- package/dist/gateway-memory.js +207 -0
- package/dist/gateway-messages.d.ts +23 -0
- package/dist/gateway-messages.js +83 -0
- package/dist/gateway-meta.d.ts +25 -0
- package/dist/gateway-meta.js +87 -0
- package/dist/gateway-provider-auto-register.d.ts +17 -0
- package/dist/gateway-provider-auto-register.js +159 -0
- package/dist/gateway-provider.d.ts +54 -0
- package/dist/gateway-provider.js +202 -0
- package/dist/gateway-rate-limiter-constants.d.ts +16 -0
- package/dist/gateway-rate-limiter-constants.js +16 -0
- package/dist/gateway-rate-limiter.d.ts +56 -0
- package/dist/gateway-rate-limiter.js +107 -0
- package/dist/gateway-retry.d.ts +49 -0
- package/dist/gateway-retry.js +204 -0
- package/dist/gateway-utils.d.ts +21 -0
- package/dist/gateway-utils.js +181 -0
- package/dist/gateway-validation.d.ts +13 -0
- package/dist/gateway-validation.js +50 -0
- package/dist/gateway.d.ts +39 -0
- package/dist/gateway.js +430 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +55 -0
- package/dist/instruction-errors.d.ts +16 -0
- package/dist/instruction-errors.js +29 -0
- package/dist/instruction-optimizer.d.ts +113 -0
- package/dist/instruction-optimizer.js +293 -0
- package/dist/instructions-parser.d.ts +31 -0
- package/dist/instructions-parser.js +56 -0
- package/dist/logger-factory.d.ts +17 -0
- package/dist/logger-factory.js +42 -0
- package/dist/message-builder.d.ts +41 -0
- package/dist/message-builder.js +522 -0
- package/dist/object-types-library-integration.d.ts +22 -0
- package/dist/object-types-library-integration.js +27 -0
- package/dist/object-types-library.d.ts +351 -0
- package/dist/object-types-library.js +210 -0
- package/dist/output-auditor.d.ts +44 -0
- package/dist/output-auditor.js +49 -0
- package/dist/request-report-generator.d.ts +60 -0
- package/dist/request-report-generator.js +169 -0
- package/dist/response-analyzer/format-type-detector.d.ts +35 -0
- package/dist/response-analyzer/format-type-detector.js +115 -0
- package/dist/response-analyzer/index.d.ts +9 -0
- package/dist/response-analyzer/index.js +8 -0
- package/dist/response-analyzer/object-type-detector.d.ts +42 -0
- package/dist/response-analyzer/object-type-detector.js +95 -0
- package/dist/response-analyzer/response-analyzer.d.ts +38 -0
- package/dist/response-analyzer/response-analyzer.js +97 -0
- package/dist/response-analyzer/types.d.ts +97 -0
- package/dist/response-analyzer/types.js +4 -0
- package/dist/response-fallback-fixer.d.ts +11 -0
- package/dist/response-fallback-fixer.js +123 -0
- package/dist/runtime-objects.d.ts +52 -0
- package/dist/runtime-objects.js +46 -0
- package/dist/template-parser.d.ts +58 -0
- package/dist/template-parser.js +99 -0
- package/dist/template-render-merge.d.ts +9 -0
- package/dist/template-render-merge.js +40 -0
- package/dist/troubleshooting-helper.d.ts +123 -0
- package/dist/troubleshooting-helper.js +596 -0
- package/dist/types.d.ts +1173 -0
- package/dist/types.js +6 -0
- package/dist/usage-tracker.d.ts +78 -0
- package/dist/usage-tracker.js +79 -0
- package/dist-cjs/activity-manager.cjs +1056 -0
- package/dist-cjs/activity-manager.d.ts +206 -0
- package/dist-cjs/config/activity-tracking-config.cjs +18 -0
- package/dist-cjs/config/activity-tracking-config.d.ts +11 -0
- package/dist-cjs/config.defaults.json +31 -0
- package/dist-cjs/content-normalizer/content-normalizer.cjs +398 -0
- package/dist-cjs/content-normalizer/content-normalizer.d.ts +46 -0
- package/dist-cjs/content-normalizer/index.cjs +12 -0
- package/dist-cjs/content-normalizer/index.d.ts +7 -0
- package/dist-cjs/content-normalizer/types.cjs +5 -0
- package/dist-cjs/content-normalizer/types.d.ts +33 -0
- package/dist-cjs/defaults/instructions-blocks.json +61 -0
- package/dist-cjs/defaults/model-config.json +16 -0
- package/dist-cjs/defaults/template-rendering.json +6 -0
- package/dist-cjs/flex-md-loader.cjs +986 -0
- package/dist-cjs/flex-md-loader.d.ts +109 -0
- package/dist-cjs/gateway-config.cjs +331 -0
- package/dist-cjs/gateway-config.d.ts +49 -0
- package/dist-cjs/gateway-conversion.cjs +212 -0
- package/dist-cjs/gateway-conversion.d.ts +29 -0
- package/dist-cjs/gateway-instructions.cjs +67 -0
- package/dist-cjs/gateway-instructions.d.ts +30 -0
- package/dist-cjs/gateway-memory.cjs +211 -0
- package/dist-cjs/gateway-memory.d.ts +51 -0
- package/dist-cjs/gateway-messages.cjs +86 -0
- package/dist-cjs/gateway-messages.d.ts +23 -0
- package/dist-cjs/gateway-meta.cjs +90 -0
- package/dist-cjs/gateway-meta.d.ts +25 -0
- package/dist-cjs/gateway-provider-auto-register.cjs +195 -0
- package/dist-cjs/gateway-provider-auto-register.d.ts +17 -0
- package/dist-cjs/gateway-provider.cjs +214 -0
- package/dist-cjs/gateway-provider.d.ts +54 -0
- package/dist-cjs/gateway-rate-limiter-constants.cjs +19 -0
- package/dist-cjs/gateway-rate-limiter-constants.d.ts +16 -0
- package/dist-cjs/gateway-rate-limiter.cjs +111 -0
- package/dist-cjs/gateway-rate-limiter.d.ts +56 -0
- package/dist-cjs/gateway-retry.cjs +212 -0
- package/dist-cjs/gateway-retry.d.ts +49 -0
- package/dist-cjs/gateway-utils.cjs +219 -0
- package/dist-cjs/gateway-utils.d.ts +21 -0
- package/dist-cjs/gateway-validation.cjs +54 -0
- package/dist-cjs/gateway-validation.d.ts +13 -0
- package/dist-cjs/gateway.cjs +434 -0
- package/dist-cjs/gateway.d.ts +39 -0
- package/dist-cjs/index.cjs +108 -0
- package/dist-cjs/index.d.ts +36 -0
- package/dist-cjs/instruction-errors.cjs +34 -0
- package/dist-cjs/instruction-errors.d.ts +16 -0
- package/dist-cjs/instruction-optimizer.cjs +299 -0
- package/dist-cjs/instruction-optimizer.d.ts +113 -0
- package/dist-cjs/instructions-parser.cjs +61 -0
- package/dist-cjs/instructions-parser.d.ts +31 -0
- package/dist-cjs/logger-factory.cjs +45 -0
- package/dist-cjs/logger-factory.d.ts +17 -0
- package/dist-cjs/message-builder.cjs +558 -0
- package/dist-cjs/message-builder.d.ts +41 -0
- package/dist-cjs/object-types-library-integration.cjs +32 -0
- package/dist-cjs/object-types-library-integration.d.ts +22 -0
- package/dist-cjs/object-types-library.cjs +215 -0
- package/dist-cjs/object-types-library.d.ts +351 -0
- package/dist-cjs/output-auditor.cjs +52 -0
- package/dist-cjs/output-auditor.d.ts +44 -0
- package/dist-cjs/request-report-generator.cjs +172 -0
- package/dist-cjs/request-report-generator.d.ts +60 -0
- package/dist-cjs/response-analyzer/format-type-detector.cjs +119 -0
- package/dist-cjs/response-analyzer/format-type-detector.d.ts +35 -0
- package/dist-cjs/response-analyzer/index.cjs +14 -0
- package/dist-cjs/response-analyzer/index.d.ts +9 -0
- package/dist-cjs/response-analyzer/object-type-detector.cjs +99 -0
- package/dist-cjs/response-analyzer/object-type-detector.d.ts +42 -0
- package/dist-cjs/response-analyzer/response-analyzer.cjs +101 -0
- package/dist-cjs/response-analyzer/response-analyzer.d.ts +38 -0
- package/dist-cjs/response-analyzer/types.cjs +5 -0
- package/dist-cjs/response-analyzer/types.d.ts +97 -0
- package/dist-cjs/response-fallback-fixer.cjs +126 -0
- package/dist-cjs/response-fallback-fixer.d.ts +11 -0
- package/dist-cjs/runtime-objects.cjs +52 -0
- package/dist-cjs/runtime-objects.d.ts +52 -0
- package/dist-cjs/template-parser.cjs +136 -0
- package/dist-cjs/template-parser.d.ts +58 -0
- package/dist-cjs/template-render-merge.cjs +43 -0
- package/dist-cjs/template-render-merge.d.ts +9 -0
- package/dist-cjs/troubleshooting-helper.cjs +611 -0
- package/dist-cjs/troubleshooting-helper.d.ts +123 -0
- package/dist-cjs/types.cjs +7 -0
- package/dist-cjs/types.d.ts +1173 -0
- package/dist-cjs/usage-tracker.cjs +83 -0
- package/dist-cjs/usage-tracker.d.ts +78 -0
- 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
|
+
}
|