pi-advisor-flow 0.2.0 → 0.2.2

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/src/config.ts CHANGED
@@ -120,6 +120,29 @@ export const setAdvisorToolResultMaxBytesRef = (value: number) => {
120
120
  advisorToolResultMaxBytesRef = value;
121
121
  };
122
122
 
123
+ /**
124
+ * Returns the current live settings state. Use this at UI boundaries instead of
125
+ * imported mutable bindings, which can be snapshotted by extension loaders.
126
+ */
127
+ export const getAdvisorSettings = () => ({
128
+ autoLoopGate: advisorAutoLoopGateRef,
129
+ blockOnBlocked: advisorBlockOnBlockedRef,
130
+ collapseResponses: advisorCollapseResponsesRef,
131
+ completionGate: advisorCompletionGateRef,
132
+ contextMaxChars: contextMaxCharsRef,
133
+ customRule: advisorCustomInvocationRef,
134
+ effort: advisorEffortRef,
135
+ failureGate: advisorFailureGateRef,
136
+ failureMode: advisorFailureModeRef,
137
+ herdrIntegration: advisorHerdrIntegrationRef,
138
+ loopThreshold: advisorLoopThresholdRef,
139
+ maxCallsPerSession: advisorMaxCallsPerSessionRef,
140
+ planGate: advisorPlanGateRef,
141
+ sessionSummary: advisorSessionSummaryRef,
142
+ toolResultMaxBytes: advisorToolResultMaxBytesRef,
143
+ toolResultMaxLines: advisorToolResultMaxLinesRef,
144
+ });
145
+
123
146
  export const splitRef = (ref: string): [string, string] => {
124
147
  const i = ref.indexOf("/");
125
148
  return i === -1 ? ["aikeys", ref] : [ref.slice(0, i), ref.slice(i + 1)];
@@ -154,127 +177,149 @@ export interface AdvisorConfig {
154
177
  gateFailureMode?: GateFailureMode;
155
178
  }
156
179
 
157
- const CONFIG_KEYS = new Set(
158
- Object.keys({
159
- advisor: true,
160
- advisorAutoLoopGate: true,
161
- advisorBlockOnBlocked: true,
162
- advisorCollapseResponses: true,
163
- advisorCompletionGate: true,
164
- advisorCustomInvocation: true,
165
- advisorEffort: true,
166
- advisorFailureGate: true,
167
- advisorHerdrIntegration: true,
168
- advisorLoopThreshold: true,
169
- advisorMaxCallsPerSession: true,
170
- advisorPlanGate: true,
171
- advisorSessionSummary: true,
172
- advisorToolResultMaxBytes: true,
173
- advisorToolResultMaxLines: true,
174
- contextMaxChars: true,
175
- executor: true,
176
- executorEffort: true,
177
- gateFailureMode: true,
178
- })
179
- );
180
+ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
181
+ "advisor",
182
+ "advisorAutoLoopGate",
183
+ "advisorBlockOnBlocked",
184
+ "advisorCollapseResponses",
185
+ "advisorCompletionGate",
186
+ "advisorCustomInvocation",
187
+ "advisorEffort",
188
+ "advisorFailureGate",
189
+ "advisorHerdrIntegration",
190
+ "advisorLoopThreshold",
191
+ "advisorMaxCallsPerSession",
192
+ "advisorPlanGate",
193
+ "advisorSessionSummary",
194
+ "advisorToolResultMaxBytes",
195
+ "advisorToolResultMaxLines",
196
+ "contextMaxChars",
197
+ "executor",
198
+ "executorEffort",
199
+ "gateFailureMode",
200
+ ]);
201
+ const BOOLEAN_CONFIG_KEYS = [
202
+ "advisorPlanGate",
203
+ "advisorFailureGate",
204
+ "advisorCompletionGate",
205
+ "advisorCollapseResponses",
206
+ "advisorBlockOnBlocked",
207
+ "advisorAutoLoopGate",
208
+ "advisorSessionSummary",
209
+ "advisorHerdrIntegration",
210
+ ] as const;
211
+ const STRING_CONFIG_KEYS = [
212
+ "executor",
213
+ "advisor",
214
+ "executorEffort",
215
+ "advisorEffort",
216
+ "advisorCustomInvocation",
217
+ ] as const;
218
+ const ARGUMENT_WHITESPACE = /\s+/;
180
219
 
181
- export const validateConfig = (
182
- value: unknown,
183
- path = "advisor.json"
184
- ): value is AdvisorConfig => {
185
- if (!value || typeof value !== "object" || Array.isArray(value)) {
186
- throw new TypeError(
187
- `Invalid advisor configuration at ${path}: expected a JSON object.`
188
- );
189
- }
190
- const config = value as Record<string, unknown>;
191
- const unknown = Object.keys(config).filter((key) => !CONFIG_KEYS.has(key));
192
- if (unknown.length) {
193
- throw new TypeError(
194
- `Invalid advisor configuration at ${path}: unknown key(s) ${unknown.map((key) => JSON.stringify(key)).join(", ")}. Remove them or upgrade pi-advisor.`
195
- );
196
- }
197
- const invalid = (key: string, accepted: string) => {
220
+ type ConfigRecord = Record<string, unknown>;
221
+
222
+ const invalidConfigValue = (
223
+ path: string,
224
+ key: string,
225
+ accepted: string
226
+ ): never => {
227
+ throw new TypeError(
228
+ `Invalid advisor configuration at ${path}, key ${JSON.stringify(key)}: expected ${accepted}.`
229
+ );
230
+ };
231
+
232
+ const validateKnownKeys = (config: ConfigRecord, path: string) => {
233
+ const unknownKeys = Object.keys(config).filter(
234
+ (key) => !CONFIG_KEYS.has(key as keyof AdvisorConfig)
235
+ );
236
+ if (unknownKeys.length > 0) {
198
237
  throw new TypeError(
199
- `Invalid advisor configuration at ${path}, key ${JSON.stringify(key)}: expected ${accepted}.`
238
+ `Invalid advisor configuration at ${path}: unknown key(s) ${unknownKeys.map((key) => JSON.stringify(key)).join(", ")}. Remove them or upgrade pi-advisor.`
200
239
  );
201
- };
202
- if (config.executor !== undefined && typeof config.executor !== "string") {
203
- invalid("executor", "a provider/model string");
204
- }
205
- if (config.advisor !== undefined && typeof config.advisor !== "string") {
206
- invalid("advisor", "a provider/model string");
207
- }
208
- if (
209
- config.executorEffort !== undefined &&
210
- typeof config.executorEffort !== "string"
211
- ) {
212
- invalid("executorEffort", "a string");
213
240
  }
214
- if (
215
- config.advisorEffort !== undefined &&
216
- typeof config.advisorEffort !== "string"
217
- ) {
218
- invalid("advisorEffort", "a string");
219
- }
220
- if (
221
- config.contextMaxChars !== undefined &&
222
- !isValidContextMaxChars(config.contextMaxChars)
223
- ) {
224
- invalid(
225
- "contextMaxChars",
226
- `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`
227
- );
241
+ };
242
+
243
+ const validateStringValues = (config: ConfigRecord, path: string) => {
244
+ for (const key of STRING_CONFIG_KEYS) {
245
+ if (config[key] !== undefined && typeof config[key] !== "string") {
246
+ invalidConfigValue(
247
+ path,
248
+ key,
249
+ key === "executor" || key === "advisor"
250
+ ? "a provider/model string"
251
+ : "a string"
252
+ );
253
+ }
228
254
  }
229
- for (const key of [
230
- "advisorPlanGate",
231
- "advisorFailureGate",
232
- "advisorCompletionGate",
233
- "advisorCollapseResponses",
234
- "advisorBlockOnBlocked",
235
- "advisorAutoLoopGate",
236
- "advisorSessionSummary",
237
- "advisorHerdrIntegration",
238
- ]) {
255
+ };
256
+
257
+ const validateBooleanValues = (config: ConfigRecord, path: string) => {
258
+ for (const key of BOOLEAN_CONFIG_KEYS) {
239
259
  if (config[key] !== undefined && typeof config[key] !== "boolean") {
240
- invalid(key, "true or false");
260
+ invalidConfigValue(path, key, "true or false");
241
261
  }
242
262
  }
243
- if (
244
- config.advisorCustomInvocation !== undefined &&
245
- typeof config.advisorCustomInvocation !== "string"
246
- ) {
247
- invalid("advisorCustomInvocation", "a string");
248
- }
249
- if (
250
- config.advisorLoopThreshold !== undefined &&
251
- !isValidLoopThreshold(config.advisorLoopThreshold)
252
- ) {
253
- invalid("advisorLoopThreshold", "a safe integer of at least 2");
263
+ };
264
+
265
+ const validateNumericValues = (config: ConfigRecord, path: string) => {
266
+ const numericRules: [
267
+ keyof AdvisorConfig,
268
+ (value: unknown) => boolean,
269
+ string,
270
+ ][] = [
271
+ [
272
+ "contextMaxChars",
273
+ isValidContextMaxChars,
274
+ `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`,
275
+ ],
276
+ [
277
+ "advisorLoopThreshold",
278
+ isValidLoopThreshold,
279
+ "a safe integer of at least 2",
280
+ ],
281
+ [
282
+ "advisorMaxCallsPerSession",
283
+ isValidMaxCallsPerSession,
284
+ "a non-negative safe integer",
285
+ ],
286
+ [
287
+ "advisorToolResultMaxLines",
288
+ isValidToolResultMaxLines,
289
+ "a non-negative safe integer",
290
+ ],
291
+ [
292
+ "advisorToolResultMaxBytes",
293
+ isValidToolResultMaxBytes,
294
+ "a non-negative safe integer",
295
+ ],
296
+ ];
297
+ for (const [key, isValid, description] of numericRules) {
298
+ if (config[key] !== undefined && !isValid(config[key])) {
299
+ invalidConfigValue(path, key, description);
300
+ }
254
301
  }
255
- if (
256
- config.advisorMaxCallsPerSession !== undefined &&
257
- !isValidMaxCallsPerSession(config.advisorMaxCallsPerSession)
258
- ) {
259
- invalid("advisorMaxCallsPerSession", "a non-negative safe integer");
302
+ };
303
+
304
+ export const validateConfig = (
305
+ value: unknown,
306
+ path = "advisor.json"
307
+ ): value is AdvisorConfig => {
308
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
309
+ throw new TypeError(
310
+ `Invalid advisor configuration at ${path}: expected a JSON object.`
311
+ );
260
312
  }
313
+ const config = value as ConfigRecord;
314
+ validateKnownKeys(config, path);
315
+ validateStringValues(config, path);
316
+ validateBooleanValues(config, path);
317
+ validateNumericValues(config, path);
261
318
  if (
262
319
  config.gateFailureMode !== undefined &&
263
320
  !isValidGateFailureMode(config.gateFailureMode)
264
321
  ) {
265
- invalid("gateFailureMode", GATE_FAILURE_MODES.join(", "));
266
- }
267
- if (
268
- config.advisorToolResultMaxLines !== undefined &&
269
- !isValidToolResultMaxLines(config.advisorToolResultMaxLines)
270
- ) {
271
- invalid("advisorToolResultMaxLines", "a non-negative safe integer");
272
- }
273
- if (
274
- config.advisorToolResultMaxBytes !== undefined &&
275
- !isValidToolResultMaxBytes(config.advisorToolResultMaxBytes)
276
- ) {
277
- invalid("advisorToolResultMaxBytes", "a non-negative safe integer");
322
+ invalidConfigValue(path, "gateFailureMode", GATE_FAILURE_MODES.join(", "));
278
323
  }
279
324
  return true;
280
325
  };
@@ -301,79 +346,109 @@ const resetDefaults = () => {
301
346
  advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
302
347
  };
303
348
 
349
+ const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
350
+ config: AdvisorConfig,
351
+ key: Key,
352
+ apply: (value: NonNullable<AdvisorConfig[Key]>) => void
353
+ ) => {
354
+ const value = config[key];
355
+ if (value !== undefined) {
356
+ apply(value as NonNullable<AdvisorConfig[Key]>);
357
+ }
358
+ };
359
+
360
+ const applyNonEmptyStringConfig = (
361
+ value: string | undefined,
362
+ apply: (value: string) => void
363
+ ) => {
364
+ if (value) {
365
+ apply(value);
366
+ }
367
+ };
368
+
369
+ const applyConfig = (config: AdvisorConfig) => {
370
+ applyNonEmptyStringConfig(config.executor, setExecutorRef);
371
+ applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
372
+ applyNonEmptyStringConfig(config.executorEffort, setExecutorEffortRef);
373
+ applyNonEmptyStringConfig(config.advisorEffort, setAdvisorEffortRef);
374
+ applyOptionalConfig(config, "contextMaxChars", setContextMaxCharsRef);
375
+ applyOptionalConfig(config, "advisorPlanGate", setAdvisorPlanGateRef);
376
+ applyOptionalConfig(config, "advisorFailureGate", setAdvisorFailureGateRef);
377
+ applyOptionalConfig(
378
+ config,
379
+ "advisorCompletionGate",
380
+ setAdvisorCompletionGateRef
381
+ );
382
+ applyOptionalConfig(
383
+ config,
384
+ "advisorCustomInvocation",
385
+ setAdvisorCustomInvocationRef
386
+ );
387
+ applyOptionalConfig(
388
+ config,
389
+ "advisorCollapseResponses",
390
+ setAdvisorCollapseResponsesRef
391
+ );
392
+ applyOptionalConfig(
393
+ config,
394
+ "advisorBlockOnBlocked",
395
+ setAdvisorBlockOnBlockedRef
396
+ );
397
+ applyOptionalConfig(config, "advisorAutoLoopGate", setAdvisorAutoLoopGateRef);
398
+ applyOptionalConfig(
399
+ config,
400
+ "advisorLoopThreshold",
401
+ setAdvisorLoopThresholdRef
402
+ );
403
+ applyOptionalConfig(
404
+ config,
405
+ "advisorMaxCallsPerSession",
406
+ setAdvisorMaxCallsPerSessionRef
407
+ );
408
+ applyOptionalConfig(
409
+ config,
410
+ "advisorSessionSummary",
411
+ setAdvisorSessionSummaryRef
412
+ );
413
+ applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
414
+ applyOptionalConfig(
415
+ config,
416
+ "advisorHerdrIntegration",
417
+ setAdvisorHerdrIntegrationRef
418
+ );
419
+ applyOptionalConfig(
420
+ config,
421
+ "advisorToolResultMaxLines",
422
+ setAdvisorToolResultMaxLinesRef
423
+ );
424
+ applyOptionalConfig(
425
+ config,
426
+ "advisorToolResultMaxBytes",
427
+ setAdvisorToolResultMaxBytesRef
428
+ );
429
+ };
430
+
431
+ const readConfig = (path: string): AdvisorConfig => {
432
+ try {
433
+ const config = JSON.parse(readFileSync(path, "utf8"));
434
+ validateConfig(config, path);
435
+ return config;
436
+ } catch (error) {
437
+ throw error instanceof Error ? error : new Error(String(error));
438
+ }
439
+ };
440
+
304
441
  export const loadConfig = (ctx: ExtensionContext) => {
305
442
  resetDefaults();
306
- for (const path of configPaths(ctx)) {
307
- if (!(path && existsSync(path))) {
308
- continue;
309
- }
310
- let config: AdvisorConfig;
311
- try {
312
- config = JSON.parse(readFileSync(path, "utf8")) as AdvisorConfig;
313
- validateConfig(config, path);
314
- } catch (error) {
315
- throw error instanceof Error ? error : new Error(String(error));
316
- }
317
- if (config.executor) {
318
- executorRef = config.executor;
319
- }
320
- if (config.advisor) {
321
- advisorRef = config.advisor;
322
- }
323
- if (config.executorEffort) {
324
- executorEffortRef = config.executorEffort;
325
- }
326
- if (config.advisorEffort) {
327
- advisorEffortRef = config.advisorEffort;
328
- }
329
- if (config.contextMaxChars !== undefined) {
330
- contextMaxCharsRef = config.contextMaxChars;
331
- }
332
- if (config.advisorPlanGate !== undefined) {
333
- advisorPlanGateRef = config.advisorPlanGate;
334
- }
335
- if (config.advisorFailureGate !== undefined) {
336
- advisorFailureGateRef = config.advisorFailureGate;
337
- }
338
- if (config.advisorCompletionGate !== undefined) {
339
- advisorCompletionGateRef = config.advisorCompletionGate;
340
- }
341
- if (config.advisorCustomInvocation !== undefined) {
342
- advisorCustomInvocationRef = config.advisorCustomInvocation || undefined;
343
- }
344
- if (config.advisorCollapseResponses !== undefined) {
345
- advisorCollapseResponsesRef = config.advisorCollapseResponses;
346
- }
347
- if (config.advisorBlockOnBlocked !== undefined) {
348
- advisorBlockOnBlockedRef = config.advisorBlockOnBlocked;
349
- }
350
- if (config.advisorAutoLoopGate !== undefined) {
351
- advisorAutoLoopGateRef = config.advisorAutoLoopGate;
352
- }
353
- if (config.advisorLoopThreshold !== undefined) {
354
- advisorLoopThresholdRef = config.advisorLoopThreshold;
355
- }
356
- if (config.advisorMaxCallsPerSession !== undefined) {
357
- advisorMaxCallsPerSessionRef = config.advisorMaxCallsPerSession;
358
- }
359
- if (config.advisorSessionSummary !== undefined) {
360
- advisorSessionSummaryRef = config.advisorSessionSummary;
361
- }
362
- if (config.gateFailureMode !== undefined) {
363
- advisorFailureModeRef = config.gateFailureMode;
364
- }
365
- if (config.advisorHerdrIntegration !== undefined) {
366
- advisorHerdrIntegrationRef = config.advisorHerdrIntegration;
367
- }
368
- if (config.advisorToolResultMaxLines !== undefined) {
369
- advisorToolResultMaxLinesRef = config.advisorToolResultMaxLines;
370
- }
371
- if (config.advisorToolResultMaxBytes !== undefined) {
372
- advisorToolResultMaxBytesRef = config.advisorToolResultMaxBytes;
373
- }
374
- return path;
443
+ const path = configPaths(ctx).find(
444
+ (candidate): candidate is string =>
445
+ candidate !== null && existsSync(candidate)
446
+ );
447
+ if (!path) {
448
+ return null;
375
449
  }
376
- return null;
450
+ applyConfig(readConfig(path));
451
+ return path;
377
452
  };
378
453
 
379
454
  export const saveConfig = (ctx: ExtensionContext) => {
@@ -391,6 +466,9 @@ export const saveConfig = (ctx: ExtensionContext) => {
391
466
  } catch {
392
467
  /* replace a missing or malformed file */
393
468
  }
469
+ if (advisorMaxCallsPerSessionRef === undefined) {
470
+ existing.advisorMaxCallsPerSession = undefined;
471
+ }
394
472
  const data = {
395
473
  ...existing,
396
474
  advisor: advisorRef,
@@ -423,7 +501,7 @@ export const parseArgs = (args: string): string | undefined => {
423
501
  let nextExecutor = executorRef;
424
502
  let nextAdvisor = advisorRef;
425
503
  let nextContextMaxChars = contextMaxCharsRef;
426
- for (const token of args.trim().split(/\s+/).filter(Boolean)) {
504
+ for (const token of args.trim().split(ARGUMENT_WHITESPACE).filter(Boolean)) {
427
505
  const [key, value] = token.split("=");
428
506
  if (key === "executor" && value) {
429
507
  nextExecutor = value;
@@ -69,30 +69,49 @@ export const capToolResult = (
69
69
  }
70
70
 
71
71
  const marker = "[... omitted tool-result section ...]";
72
- const availableBytes = Math.max(0, maxBytes - byteLength(marker) - 2);
73
- const headCount = Math.max(1, Math.floor(Math.max(1, maxLines - 1) / 2));
74
- const tailCount = Math.max(1, Math.max(1, maxLines - 1) - headCount);
75
- const head: string[] = [];
76
- let headBytes = 0;
77
- for (const line of lines.slice(0, headCount)) {
78
- const next = headBytes + byteLength(line) + (head.length ? 1 : 0);
79
- if (head.length && next > Math.ceil(availableBytes / 2)) {
80
- break;
81
- }
82
- head.push(line);
83
- headBytes = next;
84
- }
85
- const tail: string[] = [];
86
- let tailBytes = 0;
87
- const tailStart = Math.max(head.length, lines.length - tailCount);
88
- for (const line of lines.slice(tailStart)) {
89
- const next = tailBytes + byteLength(line) + (tail.length ? 1 : 0);
90
- if (tail.length && next > Math.floor(availableBytes / 2)) {
91
- break;
92
- }
93
- tail.push(line);
94
- tailBytes = next;
72
+ const markerBytes = byteLength(marker);
73
+ if (maxBytes < markerBytes || maxLines === 1) {
74
+ const content = [...marker].reduce(
75
+ (result, character) =>
76
+ byteLength(result + character) <= maxBytes
77
+ ? result + character
78
+ : result,
79
+ ""
80
+ );
81
+ return {
82
+ content,
83
+ omittedLines: totalLines,
84
+ totalBytes,
85
+ totalLines,
86
+ truncated: true,
87
+ };
95
88
  }
89
+ const headCount = Math.floor((maxLines - 1) / 2);
90
+ const tailCount = maxLines - 1 - headCount;
91
+ const collect = (
92
+ candidates: string[],
93
+ maxEntries: number,
94
+ maxContentBytes: number
95
+ ) => {
96
+ const selected: string[] = [];
97
+ let used = 0;
98
+ for (const line of candidates.slice(0, maxEntries)) {
99
+ const next = used + byteLength(line) + (selected.length ? 1 : 0);
100
+ if (next > maxContentBytes) {
101
+ break;
102
+ }
103
+ selected.push(line);
104
+ used = next;
105
+ }
106
+ return selected;
107
+ };
108
+ const availableBytes = maxBytes - markerBytes - 2;
109
+ const head = collect(lines, headCount, Math.floor(availableBytes / 2));
110
+ const tail = collect(
111
+ lines.slice(Math.max(head.length, lines.length - tailCount)),
112
+ tailCount,
113
+ availableBytes - byteLength(head.join("\n"))
114
+ );
96
115
  const content = [...head, marker, ...tail].join("\n");
97
116
  return {
98
117
  content,
@@ -150,7 +169,7 @@ const conversationEntry = (
150
169
  if (entry.type !== "message" || !isRecord(entry.message)) {
151
170
  return;
152
171
  }
153
- const message = entry.message;
172
+ const { message } = entry;
154
173
  if (message.role === "user") {
155
174
  const text = textFrom(message.content);
156
175
  return text ? `User: ${text}` : undefined;
package/src/herdr.ts CHANGED
@@ -171,10 +171,11 @@ export class HerdrAdvisorBlock {
171
171
  }
172
172
 
173
173
  clear() {
174
- if (!this.isBlocked()) {
174
+ const wasBlocked: boolean = this.#blocked;
175
+ this.#blocked = false;
176
+ if (!wasBlocked) {
175
177
  return;
176
178
  }
177
- this.#blocked = false;
178
179
  if (!this.enabled()) {
179
180
  return;
180
181
  }
@@ -196,10 +197,6 @@ export class HerdrAdvisorBlock {
196
197
  }
197
198
  }
198
199
 
199
- private isBlocked() {
200
- return this.#blocked;
201
- }
202
-
203
200
  private safeReport(labels: { blocked: string }) {
204
201
  try {
205
202
  this.report({
@@ -19,10 +19,18 @@ export interface AdvisorInvocationRecord {
19
19
  }
20
20
 
21
21
  const WHITESPACE = /\s/;
22
- const TIMESTAMP_KEY = /timestamp|time|date/;
23
- const REQUEST_ID_KEY = /requestid|correlationid|traceid/;
24
- const isVolatileKey = (key: string, pattern: RegExp) =>
25
- pattern.test(key.replace(/[-_]/g, "").toLowerCase());
22
+ const TIMESTAMP_KEYS = new Set([
23
+ "createdat",
24
+ "date",
25
+ "datetime",
26
+ "time",
27
+ "timestamp",
28
+ "updatedat",
29
+ ]);
30
+ const REQUEST_ID_KEYS = new Set(["correlationid", "requestid", "traceid"]);
31
+ const normalizedKey = (key: string) => key.replace(/[-_]/g, "").toLowerCase();
32
+ const isVolatileKey = (key: string, keys: Set<string>) =>
33
+ keys.has(normalizedKey(key));
26
34
 
27
35
  const normalizeShellWhitespace = (command: string) => {
28
36
  let result = "";
@@ -67,10 +75,10 @@ export const normalizeToolInput = (
67
75
  ): unknown => {
68
76
  const visit = (value: unknown, key?: string): unknown => {
69
77
  if (typeof value === "string") {
70
- if (key && isVolatileKey(key, TIMESTAMP_KEY)) {
78
+ if (key && isVolatileKey(key, TIMESTAMP_KEYS)) {
71
79
  return "<timestamp>";
72
80
  }
73
- if (key && isVolatileKey(key, REQUEST_ID_KEY)) {
81
+ if (key && isVolatileKey(key, REQUEST_ID_KEYS)) {
74
82
  return "<request-id>";
75
83
  }
76
84
  const normalized = normalizeString(value);