pi-advisor-flow 0.2.0 → 0.2.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/src/config.ts CHANGED
@@ -154,127 +154,149 @@ export interface AdvisorConfig {
154
154
  gateFailureMode?: GateFailureMode;
155
155
  }
156
156
 
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
- );
157
+ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
158
+ "advisor",
159
+ "advisorAutoLoopGate",
160
+ "advisorBlockOnBlocked",
161
+ "advisorCollapseResponses",
162
+ "advisorCompletionGate",
163
+ "advisorCustomInvocation",
164
+ "advisorEffort",
165
+ "advisorFailureGate",
166
+ "advisorHerdrIntegration",
167
+ "advisorLoopThreshold",
168
+ "advisorMaxCallsPerSession",
169
+ "advisorPlanGate",
170
+ "advisorSessionSummary",
171
+ "advisorToolResultMaxBytes",
172
+ "advisorToolResultMaxLines",
173
+ "contextMaxChars",
174
+ "executor",
175
+ "executorEffort",
176
+ "gateFailureMode",
177
+ ]);
178
+ const BOOLEAN_CONFIG_KEYS = [
179
+ "advisorPlanGate",
180
+ "advisorFailureGate",
181
+ "advisorCompletionGate",
182
+ "advisorCollapseResponses",
183
+ "advisorBlockOnBlocked",
184
+ "advisorAutoLoopGate",
185
+ "advisorSessionSummary",
186
+ "advisorHerdrIntegration",
187
+ ] as const;
188
+ const STRING_CONFIG_KEYS = [
189
+ "executor",
190
+ "advisor",
191
+ "executorEffort",
192
+ "advisorEffort",
193
+ "advisorCustomInvocation",
194
+ ] as const;
195
+ const ARGUMENT_WHITESPACE = /\s+/;
180
196
 
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) => {
197
+ type ConfigRecord = Record<string, unknown>;
198
+
199
+ const invalidConfigValue = (
200
+ path: string,
201
+ key: string,
202
+ accepted: string
203
+ ): never => {
204
+ throw new TypeError(
205
+ `Invalid advisor configuration at ${path}, key ${JSON.stringify(key)}: expected ${accepted}.`
206
+ );
207
+ };
208
+
209
+ const validateKnownKeys = (config: ConfigRecord, path: string) => {
210
+ const unknownKeys = Object.keys(config).filter(
211
+ (key) => !CONFIG_KEYS.has(key as keyof AdvisorConfig)
212
+ );
213
+ if (unknownKeys.length > 0) {
198
214
  throw new TypeError(
199
- `Invalid advisor configuration at ${path}, key ${JSON.stringify(key)}: expected ${accepted}.`
215
+ `Invalid advisor configuration at ${path}: unknown key(s) ${unknownKeys.map((key) => JSON.stringify(key)).join(", ")}. Remove them or upgrade pi-advisor.`
200
216
  );
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
217
  }
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
- );
218
+ };
219
+
220
+ const validateStringValues = (config: ConfigRecord, path: string) => {
221
+ for (const key of STRING_CONFIG_KEYS) {
222
+ if (config[key] !== undefined && typeof config[key] !== "string") {
223
+ invalidConfigValue(
224
+ path,
225
+ key,
226
+ key === "executor" || key === "advisor"
227
+ ? "a provider/model string"
228
+ : "a string"
229
+ );
230
+ }
228
231
  }
229
- for (const key of [
230
- "advisorPlanGate",
231
- "advisorFailureGate",
232
- "advisorCompletionGate",
233
- "advisorCollapseResponses",
234
- "advisorBlockOnBlocked",
235
- "advisorAutoLoopGate",
236
- "advisorSessionSummary",
237
- "advisorHerdrIntegration",
238
- ]) {
232
+ };
233
+
234
+ const validateBooleanValues = (config: ConfigRecord, path: string) => {
235
+ for (const key of BOOLEAN_CONFIG_KEYS) {
239
236
  if (config[key] !== undefined && typeof config[key] !== "boolean") {
240
- invalid(key, "true or false");
237
+ invalidConfigValue(path, key, "true or false");
241
238
  }
242
239
  }
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");
240
+ };
241
+
242
+ const validateNumericValues = (config: ConfigRecord, path: string) => {
243
+ const numericRules: [
244
+ keyof AdvisorConfig,
245
+ (value: unknown) => boolean,
246
+ string,
247
+ ][] = [
248
+ [
249
+ "contextMaxChars",
250
+ isValidContextMaxChars,
251
+ `a safe integer from 0 through ${MAX_CONTEXT_MAX_CHARS}`,
252
+ ],
253
+ [
254
+ "advisorLoopThreshold",
255
+ isValidLoopThreshold,
256
+ "a safe integer of at least 2",
257
+ ],
258
+ [
259
+ "advisorMaxCallsPerSession",
260
+ isValidMaxCallsPerSession,
261
+ "a non-negative safe integer",
262
+ ],
263
+ [
264
+ "advisorToolResultMaxLines",
265
+ isValidToolResultMaxLines,
266
+ "a non-negative safe integer",
267
+ ],
268
+ [
269
+ "advisorToolResultMaxBytes",
270
+ isValidToolResultMaxBytes,
271
+ "a non-negative safe integer",
272
+ ],
273
+ ];
274
+ for (const [key, isValid, description] of numericRules) {
275
+ if (config[key] !== undefined && !isValid(config[key])) {
276
+ invalidConfigValue(path, key, description);
277
+ }
254
278
  }
255
- if (
256
- config.advisorMaxCallsPerSession !== undefined &&
257
- !isValidMaxCallsPerSession(config.advisorMaxCallsPerSession)
258
- ) {
259
- invalid("advisorMaxCallsPerSession", "a non-negative safe integer");
279
+ };
280
+
281
+ export const validateConfig = (
282
+ value: unknown,
283
+ path = "advisor.json"
284
+ ): value is AdvisorConfig => {
285
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
286
+ throw new TypeError(
287
+ `Invalid advisor configuration at ${path}: expected a JSON object.`
288
+ );
260
289
  }
290
+ const config = value as ConfigRecord;
291
+ validateKnownKeys(config, path);
292
+ validateStringValues(config, path);
293
+ validateBooleanValues(config, path);
294
+ validateNumericValues(config, path);
261
295
  if (
262
296
  config.gateFailureMode !== undefined &&
263
297
  !isValidGateFailureMode(config.gateFailureMode)
264
298
  ) {
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");
299
+ invalidConfigValue(path, "gateFailureMode", GATE_FAILURE_MODES.join(", "));
278
300
  }
279
301
  return true;
280
302
  };
@@ -301,79 +323,109 @@ const resetDefaults = () => {
301
323
  advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
302
324
  };
303
325
 
326
+ const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
327
+ config: AdvisorConfig,
328
+ key: Key,
329
+ apply: (value: NonNullable<AdvisorConfig[Key]>) => void
330
+ ) => {
331
+ const value = config[key];
332
+ if (value !== undefined) {
333
+ apply(value as NonNullable<AdvisorConfig[Key]>);
334
+ }
335
+ };
336
+
337
+ const applyNonEmptyStringConfig = (
338
+ value: string | undefined,
339
+ apply: (value: string) => void
340
+ ) => {
341
+ if (value) {
342
+ apply(value);
343
+ }
344
+ };
345
+
346
+ const applyConfig = (config: AdvisorConfig) => {
347
+ applyNonEmptyStringConfig(config.executor, setExecutorRef);
348
+ applyNonEmptyStringConfig(config.advisor, setAdvisorRef);
349
+ applyNonEmptyStringConfig(config.executorEffort, setExecutorEffortRef);
350
+ applyNonEmptyStringConfig(config.advisorEffort, setAdvisorEffortRef);
351
+ applyOptionalConfig(config, "contextMaxChars", setContextMaxCharsRef);
352
+ applyOptionalConfig(config, "advisorPlanGate", setAdvisorPlanGateRef);
353
+ applyOptionalConfig(config, "advisorFailureGate", setAdvisorFailureGateRef);
354
+ applyOptionalConfig(
355
+ config,
356
+ "advisorCompletionGate",
357
+ setAdvisorCompletionGateRef
358
+ );
359
+ applyOptionalConfig(
360
+ config,
361
+ "advisorCustomInvocation",
362
+ setAdvisorCustomInvocationRef
363
+ );
364
+ applyOptionalConfig(
365
+ config,
366
+ "advisorCollapseResponses",
367
+ setAdvisorCollapseResponsesRef
368
+ );
369
+ applyOptionalConfig(
370
+ config,
371
+ "advisorBlockOnBlocked",
372
+ setAdvisorBlockOnBlockedRef
373
+ );
374
+ applyOptionalConfig(config, "advisorAutoLoopGate", setAdvisorAutoLoopGateRef);
375
+ applyOptionalConfig(
376
+ config,
377
+ "advisorLoopThreshold",
378
+ setAdvisorLoopThresholdRef
379
+ );
380
+ applyOptionalConfig(
381
+ config,
382
+ "advisorMaxCallsPerSession",
383
+ setAdvisorMaxCallsPerSessionRef
384
+ );
385
+ applyOptionalConfig(
386
+ config,
387
+ "advisorSessionSummary",
388
+ setAdvisorSessionSummaryRef
389
+ );
390
+ applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
391
+ applyOptionalConfig(
392
+ config,
393
+ "advisorHerdrIntegration",
394
+ setAdvisorHerdrIntegrationRef
395
+ );
396
+ applyOptionalConfig(
397
+ config,
398
+ "advisorToolResultMaxLines",
399
+ setAdvisorToolResultMaxLinesRef
400
+ );
401
+ applyOptionalConfig(
402
+ config,
403
+ "advisorToolResultMaxBytes",
404
+ setAdvisorToolResultMaxBytesRef
405
+ );
406
+ };
407
+
408
+ const readConfig = (path: string): AdvisorConfig => {
409
+ try {
410
+ const config = JSON.parse(readFileSync(path, "utf8"));
411
+ validateConfig(config, path);
412
+ return config;
413
+ } catch (error) {
414
+ throw error instanceof Error ? error : new Error(String(error));
415
+ }
416
+ };
417
+
304
418
  export const loadConfig = (ctx: ExtensionContext) => {
305
419
  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;
420
+ const path = configPaths(ctx).find(
421
+ (candidate): candidate is string =>
422
+ candidate !== null && existsSync(candidate)
423
+ );
424
+ if (!path) {
425
+ return null;
375
426
  }
376
- return null;
427
+ applyConfig(readConfig(path));
428
+ return path;
377
429
  };
378
430
 
379
431
  export const saveConfig = (ctx: ExtensionContext) => {
@@ -391,6 +443,9 @@ export const saveConfig = (ctx: ExtensionContext) => {
391
443
  } catch {
392
444
  /* replace a missing or malformed file */
393
445
  }
446
+ if (advisorMaxCallsPerSessionRef === undefined) {
447
+ existing.advisorMaxCallsPerSession = undefined;
448
+ }
394
449
  const data = {
395
450
  ...existing,
396
451
  advisor: advisorRef,
@@ -423,7 +478,7 @@ export const parseArgs = (args: string): string | undefined => {
423
478
  let nextExecutor = executorRef;
424
479
  let nextAdvisor = advisorRef;
425
480
  let nextContextMaxChars = contextMaxCharsRef;
426
- for (const token of args.trim().split(/\s+/).filter(Boolean)) {
481
+ for (const token of args.trim().split(ARGUMENT_WHITESPACE).filter(Boolean)) {
427
482
  const [key, value] = token.split("=");
428
483
  if (key === "executor" && value) {
429
484
  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);