@turingpulse/sdk 1.0.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 (160) hide show
  1. package/.github/dependabot.yml +38 -0
  2. package/.github/workflows/ci.yml +246 -0
  3. package/.github/workflows/framework-compat.yml +169 -0
  4. package/.github/workflows/security.yml +336 -0
  5. package/CHANGELOG.md +29 -0
  6. package/LICENSE +13 -0
  7. package/MIGRATION.md +30 -0
  8. package/README.md +221 -0
  9. package/dist/attachments.d.ts +28 -0
  10. package/dist/attachments.d.ts.map +1 -0
  11. package/dist/attachments.js +59 -0
  12. package/dist/attachments.js.map +1 -0
  13. package/dist/config.d.ts +72 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +78 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/context.d.ts +126 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +163 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/decorators.d.ts +6 -0
  22. package/dist/decorators.d.ts.map +1 -0
  23. package/dist/decorators.js +52 -0
  24. package/dist/decorators.js.map +1 -0
  25. package/dist/deploy.d.ts +89 -0
  26. package/dist/deploy.d.ts.map +1 -0
  27. package/dist/deploy.js +203 -0
  28. package/dist/deploy.js.map +1 -0
  29. package/dist/errors.d.ts +18 -0
  30. package/dist/errors.d.ts.map +1 -0
  31. package/dist/errors.js +34 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/eventBuilder.d.ts +21 -0
  34. package/dist/eventBuilder.d.ts.map +1 -0
  35. package/dist/eventBuilder.js +127 -0
  36. package/dist/eventBuilder.js.map +1 -0
  37. package/dist/fingerprint.d.ts +158 -0
  38. package/dist/fingerprint.d.ts.map +1 -0
  39. package/dist/fingerprint.js +339 -0
  40. package/dist/fingerprint.js.map +1 -0
  41. package/dist/governance.d.ts +47 -0
  42. package/dist/governance.d.ts.map +1 -0
  43. package/dist/governance.js +104 -0
  44. package/dist/governance.js.map +1 -0
  45. package/dist/http.d.ts +62 -0
  46. package/dist/http.d.ts.map +1 -0
  47. package/dist/http.js +181 -0
  48. package/dist/http.js.map +1 -0
  49. package/dist/index.d.ts +15 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +23 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/instrumentation.d.ts +40 -0
  54. package/dist/instrumentation.d.ts.map +1 -0
  55. package/dist/instrumentation.js +31 -0
  56. package/dist/instrumentation.js.map +1 -0
  57. package/dist/integrations/mastra.d.ts +64 -0
  58. package/dist/integrations/mastra.d.ts.map +1 -0
  59. package/dist/integrations/mastra.js +256 -0
  60. package/dist/integrations/mastra.js.map +1 -0
  61. package/dist/kpi.d.ts +21 -0
  62. package/dist/kpi.d.ts.map +1 -0
  63. package/dist/kpi.js +83 -0
  64. package/dist/kpi.js.map +1 -0
  65. package/dist/llmDetector.d.ts +22 -0
  66. package/dist/llmDetector.d.ts.map +1 -0
  67. package/dist/llmDetector.js +269 -0
  68. package/dist/llmDetector.js.map +1 -0
  69. package/dist/plugin.d.ts +33 -0
  70. package/dist/plugin.d.ts.map +1 -0
  71. package/dist/plugin.js +312 -0
  72. package/dist/plugin.js.map +1 -0
  73. package/dist/registry.d.ts +13 -0
  74. package/dist/registry.d.ts.map +1 -0
  75. package/dist/registry.js +18 -0
  76. package/dist/registry.js.map +1 -0
  77. package/dist/tracing.d.ts +10 -0
  78. package/dist/tracing.d.ts.map +1 -0
  79. package/dist/tracing.js +30 -0
  80. package/dist/tracing.js.map +1 -0
  81. package/dist/triggerState.d.ts +5 -0
  82. package/dist/triggerState.d.ts.map +1 -0
  83. package/dist/triggerState.js +19 -0
  84. package/dist/triggerState.js.map +1 -0
  85. package/dist/utils.d.ts +27 -0
  86. package/dist/utils.d.ts.map +1 -0
  87. package/dist/utils.js +72 -0
  88. package/dist/utils.js.map +1 -0
  89. package/package.json +37 -0
  90. package/packages/anthropic/package.json +16 -0
  91. package/packages/anthropic/src/index.ts +5 -0
  92. package/packages/anthropic/src/wrapper.ts +102 -0
  93. package/packages/anthropic/tsconfig.build.json +20 -0
  94. package/packages/langchain/package.json +16 -0
  95. package/packages/langchain/src/index.ts +7 -0
  96. package/packages/langchain/src/wrapper.ts +51 -0
  97. package/packages/mastra/package.json +17 -0
  98. package/packages/mastra/src/index.ts +8 -0
  99. package/packages/mastra/src/wrapper.ts +301 -0
  100. package/packages/openai/package.json +16 -0
  101. package/packages/openai/src/index.ts +8 -0
  102. package/packages/openai/src/wrapper.ts +103 -0
  103. package/packages/openai/tsconfig.build.json +20 -0
  104. package/packages/openclaw/openclaw.plugin.json +100 -0
  105. package/packages/openclaw/package.json +41 -0
  106. package/packages/openclaw/src/buffer.ts +99 -0
  107. package/packages/openclaw/src/config.ts +139 -0
  108. package/packages/openclaw/src/hooks/governance.ts +267 -0
  109. package/packages/openclaw/src/hooks/lifecycle.ts +75 -0
  110. package/packages/openclaw/src/hooks/telemetry.ts +207 -0
  111. package/packages/openclaw/src/index.ts +91 -0
  112. package/packages/openclaw/src/mapper.ts +233 -0
  113. package/packages/openclaw/src/session-tracker.ts +181 -0
  114. package/packages/openclaw/src/types.ts +220 -0
  115. package/packages/openclaw/tests/buffer.test.ts +148 -0
  116. package/packages/openclaw/tests/config.test.ts +122 -0
  117. package/packages/openclaw/tests/governance.test.ts +232 -0
  118. package/packages/openclaw/tests/mapper.test.ts +242 -0
  119. package/packages/openclaw/tests/session-tracker.test.ts +124 -0
  120. package/packages/openclaw/tsconfig.json +18 -0
  121. package/packages/openclaw/vitest.config.ts +8 -0
  122. package/packages/vercel-ai/package.json +16 -0
  123. package/packages/vercel-ai/src/index.ts +5 -0
  124. package/packages/vercel-ai/src/wrapper.ts +49 -0
  125. package/scripts/bump-version.sh +58 -0
  126. package/scripts/update-readme-compat.mjs +151 -0
  127. package/src/__tests__/fingerprint.test.ts +328 -0
  128. package/src/attachments.ts +88 -0
  129. package/src/config.ts +164 -0
  130. package/src/context.ts +258 -0
  131. package/src/decorators.ts +61 -0
  132. package/src/deploy.ts +260 -0
  133. package/src/errors.ts +44 -0
  134. package/src/eventBuilder.ts +153 -0
  135. package/src/fingerprint.ts +421 -0
  136. package/src/governance.ts +156 -0
  137. package/src/http.ts +241 -0
  138. package/src/index.ts +57 -0
  139. package/src/instrumentation.ts +68 -0
  140. package/src/integrations/mastra.ts +335 -0
  141. package/src/kpi.ts +112 -0
  142. package/src/llmDetector.ts +330 -0
  143. package/src/plugin.ts +384 -0
  144. package/src/registry.ts +27 -0
  145. package/src/tracing.ts +39 -0
  146. package/src/triggerState.ts +27 -0
  147. package/src/utils.ts +78 -0
  148. package/tests/compat/anthropic.test.ts +61 -0
  149. package/tests/compat/cohere.test.ts +57 -0
  150. package/tests/compat/google-genai.test.ts +61 -0
  151. package/tests/compat/langchain-openai.test.ts +41 -0
  152. package/tests/compat/langchain.test.ts +64 -0
  153. package/tests/compat/mistral.test.ts +58 -0
  154. package/tests/compat/openai.test.ts +71 -0
  155. package/tests/compat/vercel-ai.test.ts +56 -0
  156. package/tests/plugins/anthropic-wrapper.test.ts +120 -0
  157. package/tests/plugins/langchain-wrapper.test.ts +128 -0
  158. package/tests/plugins/openai-wrapper.test.ts +165 -0
  159. package/tsconfig.json +21 -0
  160. package/vitest.config.ts +9 -0
package/src/deploy.ts ADDED
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Deploy tracking for change detection and root cause analysis.
3
+ */
4
+
5
+ import { getInstance } from './plugin';
6
+ import { PluginNotInitializedError } from './errors';
7
+ import { safeEnv, safeErrorMessage } from './utils';
8
+
9
+ /**
10
+ * Information about a deployment.
11
+ */
12
+ export interface DeployInfo {
13
+ /** CI/CD provider (github, gitlab, jenkins, etc.) */
14
+ provider: string;
15
+ /** Git commit SHA */
16
+ gitSha?: string;
17
+ /** Version tag or branch name */
18
+ versionTag?: string;
19
+ /** Commit message */
20
+ commitMessage?: string;
21
+ /** Deployment timestamp */
22
+ deployedAt?: Date;
23
+ /** Additional metadata */
24
+ metadata?: Record<string, unknown>;
25
+ }
26
+
27
+ /**
28
+ * CI/CD provider detection and variable mapping.
29
+ */
30
+ const PROVIDERS: Record<
31
+ string,
32
+ {
33
+ detect: string[];
34
+ sha?: string[];
35
+ ref?: string[];
36
+ message?: string[];
37
+ [key: string]: string[] | undefined;
38
+ }
39
+ > = {
40
+ github: {
41
+ detect: ['GITHUB_ACTIONS'],
42
+ sha: ['GITHUB_SHA'],
43
+ ref: ['GITHUB_REF', 'GITHUB_REF_NAME'],
44
+ message: ['GITHUB_EVENT_HEAD_COMMIT_MESSAGE'],
45
+ run_id: ['GITHUB_RUN_ID'],
46
+ workflow: ['GITHUB_WORKFLOW'],
47
+ actor: ['GITHUB_ACTOR'],
48
+ },
49
+ gitlab: {
50
+ detect: ['GITLAB_CI'],
51
+ sha: ['CI_COMMIT_SHA'],
52
+ ref: ['CI_COMMIT_REF_NAME', 'CI_COMMIT_TAG'],
53
+ message: ['CI_COMMIT_MESSAGE'],
54
+ pipeline_id: ['CI_PIPELINE_ID'],
55
+ job_name: ['CI_JOB_NAME'],
56
+ user: ['GITLAB_USER_LOGIN'],
57
+ },
58
+ circleci: {
59
+ detect: ['CIRCLECI'],
60
+ sha: ['CIRCLE_SHA1'],
61
+ ref: ['CIRCLE_TAG', 'CIRCLE_BRANCH'],
62
+ build_num: ['CIRCLE_BUILD_NUM'],
63
+ workflow_id: ['CIRCLE_WORKFLOW_ID'],
64
+ username: ['CIRCLE_USERNAME'],
65
+ },
66
+ jenkins: {
67
+ detect: ['JENKINS_URL', 'BUILD_NUMBER'],
68
+ sha: ['GIT_COMMIT'],
69
+ ref: ['GIT_BRANCH', 'BUILD_TAG'],
70
+ build_num: ['BUILD_NUMBER'],
71
+ job_name: ['JOB_NAME'],
72
+ user: ['BUILD_USER'],
73
+ },
74
+ azure: {
75
+ detect: ['TF_BUILD', 'AZURE_PIPELINES'],
76
+ sha: ['BUILD_SOURCEVERSION'],
77
+ ref: ['BUILD_SOURCEBRANCH', 'BUILD_SOURCEBRANCHNAME'],
78
+ build_id: ['BUILD_BUILDID'],
79
+ pipeline_name: ['BUILD_DEFINITIONNAME'],
80
+ requester: ['BUILD_REQUESTEDFOR'],
81
+ },
82
+ bitbucket: {
83
+ detect: ['BITBUCKET_PIPELINE_UUID'],
84
+ sha: ['BITBUCKET_COMMIT'],
85
+ ref: ['BITBUCKET_TAG', 'BITBUCKET_BRANCH'],
86
+ build_num: ['BITBUCKET_BUILD_NUMBER'],
87
+ pipeline_uuid: ['BITBUCKET_PIPELINE_UUID'],
88
+ },
89
+ travis: {
90
+ detect: ['TRAVIS'],
91
+ sha: ['TRAVIS_COMMIT'],
92
+ ref: ['TRAVIS_TAG', 'TRAVIS_BRANCH'],
93
+ message: ['TRAVIS_COMMIT_MESSAGE'],
94
+ build_id: ['TRAVIS_BUILD_ID'],
95
+ job_id: ['TRAVIS_JOB_ID'],
96
+ },
97
+ };
98
+
99
+ /**
100
+ * Get the first non-empty environment variable value.
101
+ */
102
+ function getFirstEnv(envVars: string[]): string | undefined {
103
+ for (const varName of envVars) {
104
+ const value = safeEnv(varName);
105
+ if (value) {
106
+ return value;
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+
112
+ /**
113
+ * Automatically detect deployment info from CI/CD environment variables.
114
+ *
115
+ * Supports:
116
+ * - GitHub Actions
117
+ * - GitLab CI
118
+ * - CircleCI
119
+ * - Jenkins
120
+ * - Azure DevOps
121
+ * - Bitbucket Pipelines
122
+ * - Travis CI
123
+ */
124
+ export class AutoDeployDetector {
125
+ /**
126
+ * Detect deployment info from environment variables.
127
+ *
128
+ * @returns DeployInfo if running in a known CI/CD environment, undefined otherwise
129
+ */
130
+ static detect(): DeployInfo | undefined {
131
+ for (const [provider, config] of Object.entries(PROVIDERS)) {
132
+ const detectVars = config.detect || [];
133
+ if (!detectVars.every((v) => safeEnv(v))) {
134
+ continue;
135
+ }
136
+
137
+ const gitSha = getFirstEnv(config.sha || []);
138
+ const versionTag = getFirstEnv(config.ref || []);
139
+ const commitMessage = getFirstEnv(config.message || []);
140
+
141
+ const metadata: Record<string, string> = {};
142
+ for (const [key, envVars] of Object.entries(config)) {
143
+ if (['detect', 'sha', 'ref', 'message'].includes(key)) {
144
+ continue;
145
+ }
146
+ const value = getFirstEnv(envVars || []);
147
+ if (value) {
148
+ metadata[key] = value;
149
+ }
150
+ }
151
+
152
+ return {
153
+ provider,
154
+ gitSha,
155
+ versionTag,
156
+ commitMessage,
157
+ deployedAt: new Date(),
158
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
159
+ };
160
+ }
161
+
162
+ return undefined;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Options for registering a deploy.
168
+ */
169
+ export interface RegisterDeployOptions {
170
+ /** The workflow ID this deployment affects */
171
+ workflowId: string;
172
+ /** Version tag (e.g., "v1.2.3") */
173
+ version?: string;
174
+ /** Git commit SHA */
175
+ gitSha?: string;
176
+ /** Git commit message */
177
+ commitMessage?: string;
178
+ /** Additional metadata */
179
+ metadata?: Record<string, unknown>;
180
+ /** Deployment timestamp (defaults to now) */
181
+ deployedAt?: Date;
182
+ /** If true, auto-detect from CI/CD environment variables */
183
+ autoDetect?: boolean;
184
+ }
185
+
186
+ /**
187
+ * Register a deployment event with TuringPulse.
188
+ *
189
+ * Call this from your CI/CD pipeline or application startup to track deployments.
190
+ * Deployment events are correlated with anomalies and drifts for root cause analysis.
191
+ *
192
+ * @param options - Deploy registration options
193
+ * @returns True if deploy was registered successfully, false otherwise
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * import { registerDeploy } from '@turingpulse/sdk';
198
+ *
199
+ * // Manual registration
200
+ * await registerDeploy({
201
+ * workflowId: 'chat-assistant',
202
+ * version: 'v1.2.3',
203
+ * gitSha: process.env.GIT_SHA,
204
+ * commitMessage: process.env.GIT_COMMIT_MSG,
205
+ * });
206
+ *
207
+ * // Auto-detect from CI/CD environment
208
+ * await registerDeploy({
209
+ * workflowId: 'chat-assistant',
210
+ * autoDetect: true,
211
+ * });
212
+ * ```
213
+ */
214
+ export async function registerDeploy(options: RegisterDeployOptions): Promise<boolean> {
215
+ let { version, gitSha, commitMessage, metadata, deployedAt } = options;
216
+ const { workflowId, autoDetect = true } = options;
217
+
218
+ if (autoDetect) {
219
+ const detected = AutoDeployDetector.detect();
220
+ if (detected) {
221
+ gitSha = gitSha || detected.gitSha;
222
+ version = version || detected.versionTag;
223
+ commitMessage = commitMessage || detected.commitMessage;
224
+ deployedAt = deployedAt || detected.deployedAt;
225
+ if (detected.metadata) {
226
+ metadata = { ...(detected.metadata || {}), ...(metadata || {}) };
227
+ }
228
+ }
229
+ }
230
+
231
+ let plugin;
232
+ try {
233
+ plugin = getInstance();
234
+ } catch (error) {
235
+ if (error instanceof PluginNotInitializedError) {
236
+ // eslint-disable-next-line no-console
237
+ console.warn('Cannot register deploy: TuringPulse plugin not initialized');
238
+ return false;
239
+ }
240
+ throw error;
241
+ }
242
+
243
+ const payload = {
244
+ workflow_id: workflowId,
245
+ version_tag: version,
246
+ git_sha: gitSha,
247
+ commit_message: commitMessage,
248
+ deployed_at: (deployedAt || new Date()).toISOString(),
249
+ metadata: metadata || {},
250
+ };
251
+
252
+ try {
253
+ const result = (await plugin.client.postDeploy(payload)) as { success?: boolean } | undefined;
254
+ return result?.success ?? false;
255
+ } catch (error) {
256
+ // eslint-disable-next-line no-console
257
+ console.warn('Failed to register deploy:', safeErrorMessage(error));
258
+ return false;
259
+ }
260
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,44 @@
1
+ export class TuringPulsePluginError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'TuringPulsePluginError';
5
+ }
6
+ }
7
+
8
+ export class PluginNotInitializedError extends TuringPulsePluginError {
9
+ constructor() {
10
+ super(
11
+ 'TuringPulse plugin is not initialized. ' +
12
+ 'Call init(...) once or set TP_API_KEY env var.',
13
+ );
14
+ this.name = 'PluginNotInitializedError';
15
+ }
16
+ }
17
+
18
+ export class TriggerNotFoundError extends TuringPulsePluginError {
19
+ constructor(key: string) {
20
+ super(`No trigger registered for key "${key}".`);
21
+ this.name = 'TriggerNotFoundError';
22
+ }
23
+ }
24
+
25
+ export class GovernanceBlockedError extends TuringPulsePluginError {
26
+ readonly reason: string | undefined;
27
+ readonly policyIds: string[];
28
+
29
+ constructor(reason?: string, policyIds?: string[]) {
30
+ super(
31
+ `Execution blocked by governance policy: ${reason ?? 'no reason provided'}`,
32
+ );
33
+ this.name = 'GovernanceBlockedError';
34
+ this.reason = reason;
35
+ this.policyIds = policyIds ?? [];
36
+ }
37
+ }
38
+
39
+ export class ConfigurationError extends TuringPulsePluginError {
40
+ constructor(message: string) {
41
+ super(message);
42
+ this.name = 'ConfigurationError';
43
+ }
44
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Event construction extracted from the plugin to satisfy SRP.
3
+ *
4
+ * EventBuilder handles AgentEvent assembly from ExecutionContext,
5
+ * including metadata, tokens, tool calls, and KPI results.
6
+ */
7
+
8
+ import type { AgentEvent } from '@turingpulse/interfaces';
9
+
10
+ import { TuringPulseConfig } from './config';
11
+ import { ExecutionContext } from './context';
12
+ import { GovernanceDirective } from './governance';
13
+ import { InstrumentationOptions } from './instrumentation';
14
+ import { KPIResult, kpiMetadata } from './kpi';
15
+
16
+ const MAX_FIELD_SIZE = 500_000;
17
+
18
+ export class EventBuilder {
19
+ private readonly redactFieldNames: Set<string>;
20
+
21
+ constructor(private readonly config: TuringPulseConfig) {
22
+ this.redactFieldNames = new Set(
23
+ (config.redactFields ?? []).map((f) => f.toLowerCase()),
24
+ );
25
+ }
26
+
27
+ build(
28
+ context: ExecutionContext,
29
+ options: InstrumentationOptions,
30
+ kpiResults: KPIResult[],
31
+ ): AgentEvent {
32
+ const metadata = this.assembleMetadata(context, options, kpiResults);
33
+
34
+ return {
35
+ runId: context.runId,
36
+ agentId: context.agentId,
37
+ timestamp: (context.completedAt ?? context.startedAt).toISOString(),
38
+ type: 'agent_step',
39
+ payload: {
40
+ name: context.operation,
41
+ durationMs: context.durationMs,
42
+ status: context.status,
43
+ metadata,
44
+ tokens:
45
+ context.tokensInput > 0 || context.tokensOutput > 0
46
+ ? { prompt: context.tokensInput, completion: context.tokensOutput }
47
+ : undefined,
48
+ costUsd: context.costUsd > 0 ? context.costUsd : undefined,
49
+ },
50
+ };
51
+ }
52
+
53
+ private assembleMetadata(
54
+ context: ExecutionContext,
55
+ options: InstrumentationOptions,
56
+ kpiResults: KPIResult[],
57
+ ): Record<string, string> {
58
+ const metadata: Record<string, string> = {
59
+ ...Object.fromEntries(
60
+ Object.entries(context.labels).map(([k, v]) => [k, String(v)]),
61
+ ),
62
+ ...Object.fromEntries(
63
+ Object.entries(context.metadata).map(([k, v]) => [k, String(v)]),
64
+ ),
65
+ 'agent.status': context.status,
66
+ 'agent.hidden_entrypoint': String(context.hiddenEntrypoint),
67
+ 'agent.run_id': context.runId,
68
+ 'agent.operation': context.operation,
69
+ span_id: context.spanId,
70
+ depth: String(context.depth),
71
+ };
72
+
73
+ if (context.parentSpanId) metadata['parent_span_id'] = context.parentSpanId;
74
+ if (context.model) metadata['model'] = context.model;
75
+ if (context.provider) metadata['provider'] = context.provider;
76
+ if (context.nodeType) metadata['node_type'] = context.nodeType;
77
+ if (context.framework) metadata['framework'] = context.framework;
78
+ if (context.promptText) metadata['prompt'] = context.promptText.slice(0, MAX_FIELD_SIZE);
79
+ if (context.systemPrompt) metadata['system_prompt'] = context.systemPrompt.slice(0, MAX_FIELD_SIZE);
80
+ if (context.inputData) metadata['input'] = context.inputData.slice(0, MAX_FIELD_SIZE);
81
+ if (context.outputData) metadata['output'] = context.outputData.slice(0, MAX_FIELD_SIZE);
82
+ if (context.availableTools?.length) {
83
+ metadata['available_tools'] = JSON.stringify(context.availableTools);
84
+ }
85
+ if (context.workflowName) metadata['workflow_name'] = context.workflowName;
86
+ if (context.triggerKey) metadata['agent.trigger_key'] = context.triggerKey;
87
+
88
+ if (context.error instanceof Error) {
89
+ metadata['error.type'] = context.error.name;
90
+ metadata['error.message'] = context.error.message;
91
+ }
92
+
93
+ if (options.governance) {
94
+ metadata['governance.hitl'] = String(options.governance.hitl);
95
+ metadata['governance.hatl'] = String(options.governance.hatl);
96
+ metadata['governance.hotl'] = String(options.governance.hotl);
97
+ }
98
+
99
+ for (const result of kpiResults) {
100
+ Object.assign(metadata, kpiMetadata(result));
101
+ }
102
+
103
+ if (context.attachments.length > 0) {
104
+ metadata['attachments'] = JSON.stringify(context.attachments);
105
+ }
106
+ if (context.inputContext) {
107
+ metadata['input_context'] = context.inputContext.slice(0, MAX_FIELD_SIZE);
108
+ }
109
+
110
+ if (context.toolCalls.length > 0) {
111
+ metadata['tool_calls'] = JSON.stringify(
112
+ context.toolCalls.map((tc) => ({
113
+ tool_name: tc.toolName,
114
+ tool_args: this.redactValue(tc.toolArgs),
115
+ tool_result: tc.toolResult ?? '',
116
+ tool_id: tc.toolId ?? '',
117
+ success: tc.success,
118
+ error_message: tc.errorMessage,
119
+ })),
120
+ );
121
+ }
122
+
123
+ this.applyRedaction(metadata);
124
+
125
+ return metadata;
126
+ }
127
+
128
+ private applyRedaction(metadata: Record<string, string>): void {
129
+ if (this.redactFieldNames.size === 0) return;
130
+ for (const key of Object.keys(metadata)) {
131
+ if (this.redactFieldNames.has(key.toLowerCase())) {
132
+ metadata[key] = '[REDACTED]';
133
+ }
134
+ }
135
+ }
136
+
137
+ private redactValue(value: unknown, depth = 0): unknown {
138
+ if (depth > 20 || this.redactFieldNames.size === 0) return value;
139
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
140
+ const result: Record<string, unknown> = {};
141
+ for (const [k, v] of Object.entries(value)) {
142
+ result[k] = this.redactFieldNames.has(k.toLowerCase())
143
+ ? '[REDACTED]'
144
+ : this.redactValue(v, depth + 1);
145
+ }
146
+ return result;
147
+ }
148
+ if (Array.isArray(value)) {
149
+ return value.map((item) => this.redactValue(item, depth + 1));
150
+ }
151
+ return value;
152
+ }
153
+ }