@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
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Fingerprinting for workflow structure and change detection.
3
+ *
4
+ * The fingerprint builder captures a compact representation of every workflow
5
+ * run so the backend can automatically detect **8 change types** by comparing
6
+ * consecutive fingerprints:
7
+ *
8
+ * 1. structure — node/edge additions or removals
9
+ * 2. config — general configuration changes
10
+ * 3. prompt — system prompt changes on LLM nodes
11
+ * 4. model_change — LLM model identity or generation parameters changed
12
+ * 5. eval_config_change — evaluation / scoring / judging config changed
13
+ * 6. policy_change — guardrail / safety / moderation config changed
14
+ * 7. graph_version_change — (detected server-side from graph_snapshot_id)
15
+ * 8. deploy — (registered externally via CI/CD; not from fingerprint)
16
+ */
17
+
18
+ import { createHash } from 'node:crypto';
19
+ import { FORBIDDEN_KEYS } from './utils';
20
+
21
+ function sortKeysDeep(obj: unknown, depth = 0): unknown {
22
+ if (depth > 20 || typeof obj !== 'object' || obj === null) return obj;
23
+ if (Array.isArray(obj)) return obj.map((v) => sortKeysDeep(v, depth + 1));
24
+ const sorted: Record<string, unknown> = {};
25
+ for (const k of Object.keys(obj as Record<string, unknown>).sort()) {
26
+ if (!FORBIDDEN_KEYS.has(k)) {
27
+ sorted[k] = sortKeysDeep((obj as Record<string, unknown>)[k], depth + 1);
28
+ }
29
+ }
30
+ return sorted;
31
+ }
32
+
33
+ function sha256hex(data: string): string {
34
+ try {
35
+ return createHash('sha256').update(data, 'utf8').digest('hex');
36
+ } catch {
37
+ let hash = 5381;
38
+ for (let i = 0; i < data.length; i++) {
39
+ hash = ((hash << 5) + hash + data.charCodeAt(i)) >>> 0;
40
+ }
41
+ return hash.toString(16).padStart(16, '0');
42
+ }
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Config key classification
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const MODEL_CONFIG_KEYS = new Set([
50
+ // Identity (snake_case + camelCase across all providers)
51
+ 'model', 'model_name', 'modelname', 'model_id', 'modelid',
52
+ // Generation parameters — snake_case
53
+ 'temperature', 'top_p', 'top_k', 'max_tokens', 'max_output_tokens',
54
+ 'frequency_penalty', 'presence_penalty',
55
+ 'stop', 'stop_sequences',
56
+ 'candidate_count', 'response_format', 'context_window', 'model_kwargs',
57
+ // Generation parameters — camelCase (JS SDKs, Bedrock, Google)
58
+ 'maxtokens', 'maxoutputtokens', 'topp', 'topk',
59
+ 'frequencypenalty', 'presencepenalty',
60
+ 'stopsequences', 'candidatecount', 'responseformat',
61
+ 'contextwindow', 'modelkwargs',
62
+ // Cohere aliases (p = top_p, k = top_k)
63
+ 'p', 'k',
64
+ ]);
65
+
66
+ const EVAL_CONFIG_KEYS = new Set([
67
+ 'eval_model', 'eval_prompt', 'eval_criteria', 'eval_rubric',
68
+ 'eval_config', 'eval_threshold', 'eval_type', 'eval_template',
69
+ 'evaluation_model', 'evaluation_prompt', 'evaluation_criteria',
70
+ 'scoring_rubric', 'scoring_model', 'scoring_prompt', 'scoring_criteria',
71
+ 'judge_model', 'judge_prompt', 'judge_criteria', 'judge_config',
72
+ 'grading_rubric', 'grading_model', 'grading_criteria',
73
+ 'rubric', 'criteria', 'judge',
74
+ ]);
75
+
76
+ const POLICY_CONFIG_KEYS = new Set([
77
+ 'policy', 'policy_config', 'policy_rules', 'policy_model',
78
+ 'guard', 'guardrail', 'guardrails', 'guard_config',
79
+ 'guardrail_config', 'guardrail_rules',
80
+ 'filter', 'content_filter', 'content_filter_config',
81
+ 'moderation', 'moderation_config', 'moderation_model',
82
+ 'safety', 'safety_config', 'safety_threshold',
83
+ 'rate_limit', 'rate_limit_config',
84
+ 'max_retries', 'timeout',
85
+ ]);
86
+
87
+ /**
88
+ * Classify a config key into a hash category.
89
+ * Returns one of: "model", "eval", "policy", or "general".
90
+ */
91
+ export function classifyConfigKey(key: string): string {
92
+ const lower = key.toLowerCase();
93
+
94
+ if (MODEL_CONFIG_KEYS.has(lower)) return 'model';
95
+ if (EVAL_CONFIG_KEYS.has(lower)) return 'eval';
96
+ if (POLICY_CONFIG_KEYS.has(lower)) return 'policy';
97
+
98
+ // Prefix-based fallback
99
+ if (/^(eval_|evaluation_|scoring_|judge_|grading_)/.test(lower)) return 'eval';
100
+ if (/^(policy_|guard|guardrail|filter_|moderation_|safety_)/.test(lower)) return 'policy';
101
+
102
+ return 'general';
103
+ }
104
+
105
+ /**
106
+ * Split a flat config object into categorised sub-objects.
107
+ */
108
+ export function splitConfig(
109
+ config: Record<string, unknown>,
110
+ ): Record<string, Record<string, unknown>> {
111
+ const buckets: Record<string, Record<string, unknown>> = {};
112
+ for (const [key, value] of Object.entries(config)) {
113
+ const category = classifyConfigKey(key);
114
+ if (!buckets[category]) buckets[category] = {};
115
+ buckets[category][key] = value;
116
+ }
117
+ return buckets;
118
+ }
119
+
120
+ /**
121
+ * Configuration for fingerprinting and change detection.
122
+ */
123
+ export interface FingerprintConfig {
124
+ /** Master switch for fingerprinting. Default: true */
125
+ enabled?: boolean;
126
+ /** Hash prompts for change detection. Default: true */
127
+ capturePrompts?: boolean;
128
+ /** Hash configs for change detection. Default: true */
129
+ captureConfigs?: boolean;
130
+ /** Track DAG structure and edges. Default: true */
131
+ captureStructure?: boolean;
132
+ /** Config keys to redact before hashing. Default: ["apiKey", "password", "secret"] */
133
+ sensitiveConfigKeys?: string[];
134
+ /** Send fingerprint asynchronously. Default: true */
135
+ sendAsync?: boolean;
136
+ /** Send fingerprint even if run fails. Default: true */
137
+ sendOnFailure?: boolean;
138
+ }
139
+
140
+ /**
141
+ * Default fingerprint configuration values.
142
+ */
143
+ export const DEFAULT_FINGERPRINT_CONFIG: Required<FingerprintConfig> = {
144
+ enabled: true,
145
+ capturePrompts: true,
146
+ captureConfigs: true,
147
+ captureStructure: true,
148
+ sensitiveConfigKeys: ['apiKey', 'password', 'secret', 'token', 'key', 'credential'],
149
+ sendAsync: true,
150
+ sendOnFailure: true,
151
+ };
152
+
153
+ /**
154
+ * Fingerprint data to be sent to backend.
155
+ */
156
+ export interface FingerprintData {
157
+ /** Hash of the overall workflow structure */
158
+ structure_hash: string;
159
+ /** Ordered sequence of node names */
160
+ node_sequence: string[];
161
+ /** Count of nodes by type: {"llm": 2, "tool": 1} */
162
+ node_types: Record<string, number>;
163
+ /** Per-node type mapping: {"my_llm_node": "llm", "my_tool_node": "tool"} */
164
+ node_type_mapping: Record<string, string>;
165
+ /** Parent->child edges: ["parent->child", ...] */
166
+ edges: string[];
167
+ /** Hash of each node's full config (backward compat) */
168
+ node_config_hashes: Record<string, string>;
169
+ /** Hash of each LLM node's prompt */
170
+ node_prompt_hashes: Record<string, string>;
171
+ /** Hash of model-identity + generation params per LLM node */
172
+ node_model_hashes: Record<string, string>;
173
+ /** Hash of evaluation/scoring config per node */
174
+ node_eval_config_hashes: Record<string, string>;
175
+ /** Hash of guardrail/safety/policy config per node */
176
+ node_policy_config_hashes: Record<string, string>;
177
+ }
178
+
179
+ /**
180
+ * Builds workflow fingerprints during execution.
181
+ *
182
+ * This class collects information about nodes, their types, configs, and prompts
183
+ * during workflow execution to create a fingerprint that enables change detection.
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * const builder = new FingerprintBuilder(config);
188
+ * builder.recordNode("chat_llm", "llm", { model: "gpt-4" }, "You are a helpful assistant");
189
+ * builder.recordNode("search_tool", "tool", { api: "google" });
190
+ * const fingerprint = builder.getFingerprint();
191
+ * ```
192
+ */
193
+ export class FingerprintBuilder {
194
+ private config: Required<FingerprintConfig>;
195
+ private nodes: string[] = [];
196
+ private nodeTypes: Record<string, string> = {};
197
+ private nodeTypeCounts: Record<string, number> = {};
198
+ private edgeSet: Set<string> = new Set();
199
+ private parentStack: string[] = [];
200
+ private sensitiveKeysLower: Set<string>;
201
+
202
+ // Legacy combined config hash (backward compat)
203
+ private nodeConfigs: Record<string, string> = {};
204
+ private nodePrompts: Record<string, string> = {};
205
+
206
+ // Categorised config hashes
207
+ private nodeModelConfigs: Record<string, string> = {};
208
+ private nodeEvalConfigs: Record<string, string> = {};
209
+ private nodePolicyConfigs: Record<string, string> = {};
210
+
211
+ constructor(config: FingerprintConfig = {}) {
212
+ this.config = { ...DEFAULT_FINGERPRINT_CONFIG, ...config };
213
+ this.sensitiveKeysLower = new Set(
214
+ this.config.sensitiveConfigKeys.map((k) => k.toLowerCase())
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Push a node onto the parent stack for edge tracking.
220
+ */
221
+ pushParent(nodeName: string): void {
222
+ this.parentStack.push(nodeName);
223
+ }
224
+
225
+ /**
226
+ * Pop the current parent from the stack.
227
+ */
228
+ popParent(): string | undefined {
229
+ return this.parentStack.pop();
230
+ }
231
+
232
+ /**
233
+ * Get the current parent node (top of stack).
234
+ */
235
+ currentParent(): string | undefined {
236
+ return this.parentStack.length > 0 ? this.parentStack[this.parentStack.length - 1] : undefined;
237
+ }
238
+
239
+ /**
240
+ * Record a node in the fingerprint.
241
+ *
242
+ * The config object is automatically split into categories (model, eval,
243
+ * policy, general) and hashed separately. The combined hash is also
244
+ * retained for backward compatibility with older backend versions.
245
+ *
246
+ * @param name - Unique name for this node
247
+ * @param nodeType - Type of node (e.g., "llm", "tool", "function", "retriever")
248
+ * @param config - Configuration dict for the node (will be categorised and hashed)
249
+ * @param prompt - System prompt for LLM nodes (will be hashed)
250
+ */
251
+ recordNode(
252
+ name: string,
253
+ nodeType: string,
254
+ config?: unknown,
255
+ prompt?: string
256
+ ): void {
257
+ if (this.nodeTypes[name] !== undefined) {
258
+ return;
259
+ }
260
+
261
+ this.nodes.push(name);
262
+ this.nodeTypes[name] = nodeType;
263
+ this.nodeTypeCounts[nodeType] = (this.nodeTypeCounts[nodeType] || 0) + 1;
264
+
265
+ // Record edge from parent
266
+ if (this.config.captureStructure) {
267
+ const parent = this.currentParent();
268
+ if (parent) {
269
+ this.edgeSet.add(`${parent}->${name}`);
270
+ }
271
+ }
272
+
273
+ // Hash and store config (both combined and per-category)
274
+ if (this.config.captureConfigs && config !== undefined) {
275
+ const sanitized = this.sanitizeConfig(config);
276
+
277
+ // Legacy combined hash
278
+ this.nodeConfigs[name] = this.hashValue(sanitized);
279
+
280
+ // Categorised hashes
281
+ if (typeof sanitized === 'object' && sanitized !== null && !Array.isArray(sanitized)) {
282
+ const buckets = splitConfig(sanitized as Record<string, unknown>);
283
+ if (buckets.model) this.nodeModelConfigs[name] = this.hashValue(buckets.model);
284
+ if (buckets.eval) this.nodeEvalConfigs[name] = this.hashValue(buckets.eval);
285
+ if (buckets.policy) this.nodePolicyConfigs[name] = this.hashValue(buckets.policy);
286
+ }
287
+ }
288
+
289
+ // Hash and store prompt
290
+ if (this.config.capturePrompts && prompt !== undefined) {
291
+ this.nodePrompts[name] = this.hashValue(prompt);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Build and return the final fingerprint data.
297
+ */
298
+ getFingerprint(): FingerprintData {
299
+ const structureData = {
300
+ nodes: this.nodes,
301
+ node_types: this.nodeTypes,
302
+ edges: [...this.edgeSet],
303
+ };
304
+ const structureHash = this.hashValue(structureData);
305
+
306
+ return {
307
+ structure_hash: structureHash,
308
+ node_sequence: [...this.nodes],
309
+ node_types: { ...this.nodeTypeCounts },
310
+ node_type_mapping: { ...this.nodeTypes },
311
+ edges: [...this.edgeSet],
312
+ node_config_hashes: { ...this.nodeConfigs },
313
+ node_prompt_hashes: { ...this.nodePrompts },
314
+ node_model_hashes: { ...this.nodeModelConfigs },
315
+ node_eval_config_hashes: { ...this.nodeEvalConfigs },
316
+ node_policy_config_hashes: { ...this.nodePolicyConfigs },
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Remove sensitive keys from config before hashing.
322
+ */
323
+ private sanitizeConfig(config: unknown, depth = 0): unknown {
324
+ if (depth > 20 || typeof config !== 'object' || config === null) {
325
+ return config;
326
+ }
327
+
328
+ if (Array.isArray(config)) {
329
+ return config.map((item) => this.sanitizeConfig(item, depth + 1));
330
+ }
331
+
332
+ const sanitized: Record<string, unknown> = Object.create(null);
333
+ for (const [key, value] of Object.entries(config as Record<string, unknown>)) {
334
+ if (FORBIDDEN_KEYS.has(key)) continue;
335
+ if (this.sensitiveKeysLower.has(key.toLowerCase())) {
336
+ sanitized[key] = '[REDACTED]';
337
+ } else if (typeof value === 'object' && value !== null) {
338
+ sanitized[key] = this.sanitizeConfig(value, depth + 1);
339
+ } else {
340
+ sanitized[key] = value;
341
+ }
342
+ }
343
+
344
+ return sanitized;
345
+ }
346
+
347
+ /**
348
+ * Hash a value using the configured algorithm.
349
+ */
350
+ private hashValue(value: unknown): string {
351
+ let data: string;
352
+ if (typeof value === 'string') {
353
+ data = value;
354
+ } else {
355
+ try {
356
+ data = JSON.stringify(sortKeysDeep(value));
357
+ } catch {
358
+ data = String(value);
359
+ }
360
+ }
361
+
362
+ return sha256hex(data).substring(0, 16);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Extract system prompt from a list of chat messages.
368
+ *
369
+ * @param messages - List of message objects with 'role' and 'content' keys
370
+ * @returns The system prompt if found, otherwise undefined
371
+ */
372
+ export function extractPromptFromMessages(
373
+ messages: Array<{ role?: string; content?: string | unknown[] }>
374
+ ): string | undefined {
375
+ if (!messages || !Array.isArray(messages)) {
376
+ return undefined;
377
+ }
378
+
379
+ for (const msg of messages) {
380
+ if (msg.role === 'system') {
381
+ const content = msg.content;
382
+ if (typeof content === 'string') {
383
+ return content;
384
+ } else if (Array.isArray(content)) {
385
+ // Handle structured content
386
+ const parts: string[] = [];
387
+ for (const part of content) {
388
+ if (typeof part === 'string') {
389
+ parts.push(part);
390
+ } else if (typeof part === 'object' && part !== null && 'text' in part) {
391
+ parts.push((part as { text: string }).text);
392
+ }
393
+ }
394
+ return parts.length > 0 ? parts.join(' ') : undefined;
395
+ }
396
+ }
397
+ }
398
+
399
+ return undefined;
400
+ }
401
+
402
+ /**
403
+ * Extract a subset of config keys from an object.
404
+ *
405
+ * @param obj - Full object
406
+ * @param keys - Keys to extract
407
+ * @returns Object with only the specified keys that exist
408
+ */
409
+ export function extractConfigSubset(
410
+ obj: Record<string, unknown>,
411
+ keys: string[]
412
+ ): Record<string, unknown> {
413
+ const result: Record<string, unknown> = {};
414
+ for (const key of keys) {
415
+ if (key in obj && obj[key] !== undefined) {
416
+ result[key] = obj[key];
417
+ }
418
+ }
419
+ return result;
420
+ }
421
+
@@ -0,0 +1,156 @@
1
+ import { TuringPulseConfig } from './config';
2
+ import { ExecutionContext } from './context';
3
+ import { TuringPulseHttpClient } from './http';
4
+ import { KPIResult, kpiMetadata } from './kpi';
5
+ import { safeErrorMessage } from './utils';
6
+
7
+ export type GovernanceMode = 'hitl' | 'hatl' | 'hotl';
8
+
9
+ export interface GovernanceDirectiveOptions {
10
+ hitl?: boolean;
11
+ hatl?: boolean;
12
+ hotl?: boolean;
13
+ reviewers?: string[];
14
+ escalationChannels?: string[];
15
+ title?: string;
16
+ notes?: string;
17
+ metadata?: Record<string, string>;
18
+ severity?: string;
19
+ /** Escalation timeout — enforced by the backend, not the SDK. */
20
+ autoEscalateAfterSeconds?: number;
21
+ taskPrefix?: string;
22
+ }
23
+
24
+ export class GovernanceDirective {
25
+ readonly hitl: boolean;
26
+ readonly hatl: boolean;
27
+ readonly hotl: boolean;
28
+ readonly reviewers: string[];
29
+ readonly escalationChannels: string[];
30
+ readonly title?: string;
31
+ readonly notes?: string;
32
+ readonly metadata: Record<string, string>;
33
+ readonly severity: string;
34
+ readonly autoEscalateAfterSeconds?: number;
35
+ readonly taskPrefix?: string;
36
+
37
+ constructor(options: GovernanceDirectiveOptions = {}) {
38
+ this.hitl = Boolean(options.hitl);
39
+ this.hatl = Boolean(options.hatl);
40
+ this.hotl = Boolean(options.hotl);
41
+ this.reviewers = options.reviewers ?? [];
42
+ this.escalationChannels = options.escalationChannels ?? [];
43
+ this.title = options.title;
44
+ this.notes = options.notes;
45
+ this.metadata = { ...(options.metadata ?? {}) };
46
+ this.severity = options.severity ?? 'medium';
47
+ this.autoEscalateAfterSeconds = options.autoEscalateAfterSeconds;
48
+ this.taskPrefix = options.taskPrefix;
49
+ }
50
+
51
+ modes(): GovernanceMode[] {
52
+ const modes: GovernanceMode[] = [];
53
+ if (this.hitl) modes.push('hitl');
54
+ if (this.hatl) modes.push('hatl');
55
+ if (this.hotl) modes.push('hotl');
56
+ return modes;
57
+ }
58
+ }
59
+
60
+ export interface GovernanceTaskResult {
61
+ mode: GovernanceMode;
62
+ taskId?: string;
63
+ }
64
+
65
+ export class GovernanceManager {
66
+ constructor(private readonly client: TuringPulseHttpClient, private readonly config: TuringPulseConfig) {}
67
+
68
+ async enforce(
69
+ directive: GovernanceDirective | undefined,
70
+ context: ExecutionContext,
71
+ kpiResults: KPIResult[],
72
+ ): Promise<GovernanceTaskResult[]> {
73
+ if (!directive) {
74
+ return [];
75
+ }
76
+ const metadata = this.buildMetadata(directive, context, kpiResults);
77
+ const tasks: GovernanceTaskResult[] = [];
78
+ for (const mode of directive.modes()) {
79
+ const payload = this.buildTaskPayload(mode, directive, context, metadata);
80
+ try {
81
+ const taskId = await this.client.createTask(payload);
82
+ tasks.push({ mode, taskId });
83
+ } catch (error) {
84
+ // eslint-disable-next-line no-console
85
+ console.warn(`Failed to create governance task for ${mode}:`, safeErrorMessage(error));
86
+ }
87
+ }
88
+ return tasks;
89
+ }
90
+
91
+ private buildMetadata(
92
+ directive: GovernanceDirective,
93
+ context: ExecutionContext,
94
+ kpiResults: KPIResult[],
95
+ ): Record<string, string> {
96
+ const metadata: Record<string, string> = {
97
+ ...this.config.governanceDefaults.metadata,
98
+ ...directive.metadata,
99
+ severity: directive.severity || this.config.governanceDefaults.severity,
100
+ 'agent.run_id': context.runId,
101
+ 'agent.operation': context.operation,
102
+ 'agent.hidden_entrypoint': String(context.hiddenEntrypoint),
103
+ };
104
+ for (const result of kpiResults) {
105
+ Object.assign(metadata, kpiMetadata(result));
106
+ }
107
+ return metadata;
108
+ }
109
+
110
+ private buildTaskPayload(
111
+ mode: GovernanceMode,
112
+ directive: GovernanceDirective,
113
+ context: ExecutionContext,
114
+ metadata: Record<string, string>,
115
+ ): Record<string, unknown> {
116
+ const title = directive.title ?? `${mode.toUpperCase()} review for ${context.operation}`;
117
+ const taskIdPrefix = directive.taskPrefix ?? context.runId;
118
+ const reviewers = directive.reviewers.length ? directive.reviewers : this.config.governanceDefaults.reviewers;
119
+ const channels = directive.escalationChannels.length
120
+ ? directive.escalationChannels
121
+ : this.config.governanceDefaults.escalationChannels;
122
+
123
+ return {
124
+ taskId: `${taskIdPrefix}-${mode}`,
125
+ title,
126
+ status: defaultTaskStatus(mode),
127
+ agentRunId: context.runId,
128
+ hitlRequired: mode === 'hitl',
129
+ metadata: {
130
+ ...metadata,
131
+ 'governance.mode': mode,
132
+ 'governance.reviewers': reviewers.join(','),
133
+ 'governance.channels': channels.join(','),
134
+ ...(directive.notes ? { 'governance.notes': directive.notes } : {}),
135
+ ...((directive.autoEscalateAfterSeconds ?? this.config.governanceDefaults.autoEscalateAfterSeconds) != null
136
+ ? {
137
+ 'governance.auto_escalate_after_seconds': String(
138
+ directive.autoEscalateAfterSeconds ?? this.config.governanceDefaults.autoEscalateAfterSeconds,
139
+ ),
140
+ }
141
+ : {}),
142
+ },
143
+ };
144
+ }
145
+ }
146
+
147
+ function defaultTaskStatus(mode: GovernanceMode): string {
148
+ switch (mode) {
149
+ case 'hitl':
150
+ return 'needs_human';
151
+ case 'hotl':
152
+ return 'in_progress';
153
+ default:
154
+ return 'todo';
155
+ }
156
+ }