pi-advisor-flow 0.7.0 → 0.8.0

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 (92) hide show
  1. package/CHANGELOG.md +19 -18
  2. package/README.md +9 -8
  3. package/dist/index.js +1390 -1068
  4. package/extensions/index.ts +2 -1
  5. package/package.json +48 -35
  6. package/src/attachments.ts +11 -10
  7. package/src/commands/activation-preparation.ts +1 -0
  8. package/src/commands/activation.ts +2 -0
  9. package/src/commands/lifecycle.ts +1 -0
  10. package/src/commands/manual-command.ts +7 -1
  11. package/src/commands/manual-consultation.ts +102 -92
  12. package/src/commands/manual-progress.ts +7 -5
  13. package/src/commands/model-commands.ts +1 -1
  14. package/src/commands/model-options.ts +14 -3
  15. package/src/commands/model-picker.ts +1 -0
  16. package/src/commands/registration.ts +1 -0
  17. package/src/commands/renderers.ts +67 -55
  18. package/src/commands/runtime.ts +16 -14
  19. package/src/commands/settings-commands.ts +10 -4
  20. package/src/commands/settings-persistence.ts +16 -2
  21. package/src/commands/types.ts +1 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +8 -1
  25. package/src/config/schema.ts +42 -17
  26. package/src/config/state.ts +12 -3
  27. package/src/config/storage.ts +34 -23
  28. package/src/config/types.ts +16 -3
  29. package/src/config/validation.ts +33 -21
  30. package/src/config.ts +3 -0
  31. package/src/content-utils.ts +24 -4
  32. package/src/conversation.ts +16 -15
  33. package/src/git.ts +10 -3
  34. package/src/herdr-block.ts +83 -0
  35. package/src/herdr-shared.ts +108 -0
  36. package/src/herdr.ts +30 -192
  37. package/src/jev/client.ts +75 -85
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +26 -11
  40. package/src/jev/ledger.ts +58 -64
  41. package/src/jev/questions.ts +26 -23
  42. package/src/jev/state.ts +15 -12
  43. package/src/jev/transport.ts +8 -3
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +15 -11
  46. package/src/pi-settings.ts +4 -9
  47. package/src/preferences.ts +4 -2
  48. package/src/redaction.ts +10 -12
  49. package/src/scout-context.ts +59 -60
  50. package/src/scout-curation.ts +4 -13
  51. package/src/scout-groups.ts +178 -175
  52. package/src/scout-protocol.ts +74 -70
  53. package/src/scout-reconstruct.ts +1 -1
  54. package/src/scout.ts +115 -73
  55. package/src/session-state.ts +64 -49
  56. package/src/tool-result-cap.ts +18 -17
  57. package/src/tools/consult-context.ts +7 -15
  58. package/src/tools/consultation.ts +14 -4
  59. package/src/tools/gate-policy.ts +1 -0
  60. package/src/tools/gate-protocol.ts +36 -23
  61. package/src/tools/jev-filter.ts +43 -41
  62. package/src/tools/jev-turn-gate.ts +29 -9
  63. package/src/tools/loop-gate.ts +31 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/prompts.ts +20 -25
  66. package/src/tools/register-ask-advisor.ts +36 -16
  67. package/src/tools/register-lifecycle.ts +22 -0
  68. package/src/tools/register-outcome.ts +2 -0
  69. package/src/tools/register-renderers.ts +146 -128
  70. package/src/tools/registration.ts +12 -14
  71. package/src/tools/render-advisor-result.ts +35 -22
  72. package/src/tools/render-common.ts +16 -19
  73. package/src/tools/scout-status.ts +42 -27
  74. package/src/tools/types.ts +1 -0
  75. package/src/tools.ts +0 -1
  76. package/src/ui/jev-setup-submenu.ts +65 -59
  77. package/src/ui/manual-dialog-render.ts +1 -0
  78. package/src/ui/manual-dialog.ts +39 -43
  79. package/src/ui/masked-input.ts +5 -11
  80. package/src/ui/model-multi-selector.ts +9 -0
  81. package/src/ui/model-selector-adapter.ts +31 -0
  82. package/src/ui/model-selector.ts +14 -161
  83. package/src/ui/searchable-model-list.ts +230 -0
  84. package/src/ui/settings-formatting.ts +24 -14
  85. package/src/ui/settings-items.ts +38 -1
  86. package/src/ui/settings-list-adapter.ts +15 -12
  87. package/src/ui/settings-mutations.ts +157 -70
  88. package/src/ui/settings-selector.ts +18 -9
  89. package/src/ui/text-setting-submenu.ts +3 -6
  90. package/src/ui/types.ts +14 -0
  91. package/src/ui.ts +0 -1
  92. package/src/usage.ts +26 -19
@@ -1,115 +1,202 @@
1
1
  import { DEFAULT_JEV_MODEL } from "../config/types.ts";
2
- import type { AdvisorSettings, ContextPreset, SettingValue } from "./types.ts";
2
+ import type { AdvisorSettings, ContextPreset } from "./types.ts";
3
3
 
4
- const BOOLEAN_SETTING_IDS = new Set([
5
- "scoutEnabled",
6
- "showUsageDetails",
7
- "showUsageFooter",
8
- "planGate",
9
- "failureGate",
10
- "completionGate",
11
- "collapseResponses",
12
- "blockOnBlocked",
4
+ const BOOLEAN_SETTING_FIELDS = [
13
5
  "autoLoopGate",
14
- "sessionSummary",
6
+ "blockOnBlocked",
7
+ "collapseResponses",
8
+ "completionGate",
9
+ "failureGate",
15
10
  "herdrIntegration",
11
+ "outcomeLogging",
12
+ "planGate",
16
13
  "redactSecrets",
14
+ "scoutEnabled",
15
+ "sessionSummary",
16
+ "showUsageDetails",
17
+ "showUsageFooter",
17
18
  "trackedFileContent",
18
19
  "untrackedContent",
19
- "outcomeLogging",
20
- ]);
20
+ ] as const;
21
21
 
22
- export const mutateAdvisorSettings = (
22
+ const BOOLEAN_SETTING_IDS = new Set<string>(BOOLEAN_SETTING_FIELDS);
23
+
24
+ type BooleanSettingField = (typeof BOOLEAN_SETTING_FIELDS)[number];
25
+
26
+ const parseModelWhitelist = (value: string) => [
27
+ ...new Set(
28
+ value
29
+ .split(",")
30
+ .map((model) => model.trim())
31
+ .filter(Boolean)
32
+ ),
33
+ ];
34
+
35
+ const applyCoreMutation = (
23
36
  settings: AdvisorSettings,
24
37
  id: string,
25
38
  value: string,
26
39
  presets: ContextPreset[]
27
- ): void => {
40
+ ): boolean => {
28
41
  switch (id) {
29
- case "context":
42
+ case "context": {
30
43
  settings.contextMaxChars =
31
44
  presets.find((preset) => preset.label === value)?.value ??
32
45
  settings.contextMaxChars;
33
- break;
34
- case "simpleMode":
46
+ return true;
47
+ }
48
+ case "simpleMode": {
35
49
  settings.simpleMode = value === "On";
36
- break;
37
- case "alwaysOn":
50
+ return true;
51
+ }
52
+ case "alwaysOn": {
38
53
  settings.alwaysOn = value === "On";
39
- break;
40
- case "effort":
54
+ return true;
55
+ }
56
+ case "effort": {
41
57
  settings.effort = value;
42
- break;
43
- case "customRule":
58
+ return true;
59
+ }
60
+ case "customRule": {
44
61
  settings.customRule = value.trim() || undefined;
45
- break;
46
- case "toolPolicies":
62
+ return true;
63
+ }
64
+ case "toolPolicies": {
65
+ // SAFETY: the value originates from the settings submenu's previously validated JSON editor.
47
66
  settings.toolPolicies = JSON.parse(
48
67
  value
49
68
  ) as AdvisorSettings["toolPolicies"];
50
- break;
51
- case "loopThreshold":
69
+ return true;
70
+ }
71
+ case "loopThreshold": {
52
72
  settings.loopThreshold = Number(
53
73
  value.replace("After ", "").replace(" repeats", "")
54
74
  );
55
- break;
56
- case "maxCallsPerSession":
75
+ return true;
76
+ }
77
+ case "maxCallsPerSession": {
57
78
  settings.maxCallsPerSession = value === "∞" ? undefined : Number(value);
58
- break;
59
- case "failureMode":
79
+ return true;
80
+ }
81
+ case "modelWhitelist": {
82
+ settings.modelWhitelist = parseModelWhitelist(value);
83
+ return true;
84
+ }
85
+ case "failureMode": {
86
+ // SAFETY: value comes from the fixed failureMode options of the settings list.
60
87
  settings.failureMode = value as AdvisorSettings["failureMode"];
61
- break;
62
- case "gitContext":
88
+ return true;
89
+ }
90
+ case "gitContext": {
91
+ // SAFETY: value comes from the fixed GitContextLevel options of the settings list.
63
92
  settings.gitContext = value as AdvisorSettings["gitContext"];
64
- break;
65
- case "toolResultMaxLines":
93
+ return true;
94
+ }
95
+ default: {
96
+ return false;
97
+ }
98
+ }
99
+ };
100
+
101
+ const applyNumericMutation = (
102
+ settings: AdvisorSettings,
103
+ id: string,
104
+ value: string
105
+ ): boolean => {
106
+ switch (id) {
107
+ case "toolResultMaxLines": {
66
108
  settings.toolResultMaxLines = Number(value);
67
- break;
68
- case "toolResultMaxBytes":
109
+ return true;
110
+ }
111
+ case "toolResultMaxBytes": {
69
112
  settings.toolResultMaxBytes = Number(value);
70
- break;
71
- case "gitContextMaxChars":
113
+ return true;
114
+ }
115
+ case "gitContextMaxChars": {
72
116
  settings.gitContextMaxChars = Number(value);
73
- break;
74
- case "jevFilter":
117
+ return true;
118
+ }
119
+ default: {
120
+ return false;
121
+ }
122
+ }
123
+ };
124
+
125
+ const applyJevMutation = (
126
+ settings: AdvisorSettings,
127
+ id: string,
128
+ value: string
129
+ ): boolean => {
130
+ switch (id) {
131
+ case "jevFilter": {
75
132
  settings.jevFilterEnabled = value === "On";
76
- break;
77
- case "jevFilterSkipConfidence":
133
+ return true;
134
+ }
135
+ case "jevFilterSkipConfidence": {
78
136
  settings.jevFilterSkipConfidence = Number(value);
79
- break;
80
- case "jevFilterNoulMargin":
137
+ return true;
138
+ }
139
+ case "jevFilterNoulMargin": {
81
140
  settings.jevFilterNoulMargin = Number(value);
82
- break;
83
- case "jevFilterOverrideWindow":
141
+ return true;
142
+ }
143
+ case "jevFilterOverrideWindow": {
84
144
  settings.jevFilterOverrideWindow = Number(value.replace(" turns", ""));
85
- break;
86
- case "jevTurnGateEveryTurns":
145
+ return true;
146
+ }
147
+ case "jevTurnGateEveryTurns": {
87
148
  settings.jevTurnGateEveryTurns =
88
- value === "Off" ? 0 : Number(value.replace(/[^0-9]/g, ""));
89
- break;
90
- case "jevTurnGateNoulThreshold":
149
+ value === "Off" ? 0 : Number(value.replaceAll(/[^0-9]/gu, ""));
150
+ return true;
151
+ }
152
+ case "jevTurnGateNoulThreshold": {
91
153
  settings.jevTurnGateNoulThreshold = Number(value);
92
- break;
93
- case "jevModel":
154
+ return true;
155
+ }
156
+ case "jevModel": {
94
157
  settings.jevModel = value.trim() || DEFAULT_JEV_MODEL;
95
- break;
96
- case "jevTimeoutMs":
158
+ return true;
159
+ }
160
+ case "jevTimeoutMs": {
97
161
  settings.jevTimeoutMs = Number(value);
98
- break;
99
- case "jevDigestMaxChars":
162
+ return true;
163
+ }
164
+ case "jevDigestMaxChars": {
100
165
  settings.jevDigestMaxChars = Number(value);
101
- break;
102
- case "jevPricePerMtok":
166
+ return true;
167
+ }
168
+ case "jevPricePerMtok": {
103
169
  settings.jevPricePerMtok = Number(value);
104
- break;
105
- case "jevTransport":
170
+ return true;
171
+ }
172
+ case "jevTransport": {
173
+ // SAFETY: value comes from the fixed jevTransport options of the settings list.
106
174
  settings.jevTransport = value as AdvisorSettings["jevTransport"];
107
- break;
108
- default:
109
- if (BOOLEAN_SETTING_IDS.has(id)) {
110
- (settings as unknown as Record<string, SettingValue | boolean>)[id] =
111
- value === "On";
112
- }
113
- break;
175
+ return true;
176
+ }
177
+ default: {
178
+ return false;
179
+ }
180
+ }
181
+ };
182
+
183
+ export const mutateAdvisorSettings = (
184
+ settings: AdvisorSettings,
185
+ id: string,
186
+ value: string,
187
+ presets: ContextPreset[]
188
+ ): void => {
189
+ if (applyCoreMutation(settings, id, value, presets)) {
190
+ return;
191
+ }
192
+ if (applyNumericMutation(settings, id, value)) {
193
+ return;
194
+ }
195
+ if (applyJevMutation(settings, id, value)) {
196
+ return;
197
+ }
198
+ if (BOOLEAN_SETTING_IDS.has(id)) {
199
+ // SAFETY: BOOLEAN_SETTING_IDS only contains AdvisorSettings boolean fields.
200
+ settings[id as BooleanSettingField] = value === "On";
114
201
  }
115
202
  };
@@ -1,15 +1,15 @@
1
1
  import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
2
- import {
3
- type Component,
4
- type Focusable,
5
- SettingsList,
6
- truncateToWidth,
7
- } from "@earendil-works/pi-tui";
2
+ import { SettingsList, truncateToWidth } from "@earendil-works/pi-tui";
3
+ import type { Component, Focusable } from "@earendil-works/pi-tui";
4
+
8
5
  import {
9
6
  rainbowGradient,
10
7
  SIMPLE_MODE_GRADIENT_INTERVAL_MS,
11
8
  } from "./settings-formatting.ts";
12
- import { createSettingsItems } from "./settings-items.ts";
9
+ import {
10
+ advisorModelWhitelistItem,
11
+ createSettingsItems,
12
+ } from "./settings-items.ts";
13
13
  import { SettingsListAdapter } from "./settings-list-adapter.ts";
14
14
  import { mutateAdvisorSettings } from "./settings-mutations.ts";
15
15
  import type {
@@ -51,7 +51,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
51
51
  label: String(configuredContext),
52
52
  value: configuredContext,
53
53
  },
54
- ].sort((a, b) => a.value - b.value);
54
+ ].toSorted((a, b) => a.value - b.value);
55
55
  if (this.settings.simpleMode) {
56
56
  this.startSimpleModeGradient();
57
57
  }
@@ -96,8 +96,16 @@ export class AdvisorSettingsSelector implements Component, Focusable {
96
96
  }
97
97
  return defaultLabel(text, selected);
98
98
  };
99
+ const modelWhitelist = advisorModelWhitelistItem(
100
+ this.settings,
101
+ this.options.modelRefs,
102
+ this.options.keybindings,
103
+ this.options.theme,
104
+ this.options.tui
105
+ );
99
106
  const items = createSettingsItems({
100
107
  effortLevels: this.options.effortLevels,
108
+ modelWhitelist,
101
109
  presets: this.presets,
102
110
  settings: this.settings,
103
111
  theme: this.options.theme,
@@ -147,12 +155,13 @@ export class AdvisorSettingsSelector implements Component, Focusable {
147
155
  (this.options.onChange ?? this.options.onSave)?.({
148
156
  ...this.settings,
149
157
  showUsageDetails: this.settings.showUsageDetails ?? true,
150
- toolPolicies: { ...(this.settings.toolPolicies ?? {}) },
158
+ toolPolicies: { ...this.settings.toolPolicies },
151
159
  });
152
160
  if (
153
161
  id === "context" ||
154
162
  id === "simpleMode" ||
155
163
  id === "customRule" ||
164
+ id === "modelWhitelist" ||
156
165
  id === "toolPolicies"
157
166
  ) {
158
167
  this.settingsList = this.createSettingsList(id);
@@ -1,9 +1,6 @@
1
- import {
2
- type Component,
3
- type Focusable,
4
- Input,
5
- truncateToWidth,
6
- } from "@earendil-works/pi-tui";
1
+ import { Input, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import type { Component, Focusable } from "@earendil-works/pi-tui";
3
+
7
4
  import type { TextSettingSubmenuOptions } from "./types.ts";
8
5
 
9
6
  /** Small inline editor used by SettingsList for the two free-form settings. */
package/src/ui/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
3
+
3
4
  import type { GateFailureMode } from "../config/types.ts";
4
5
  import type { GitContextLevel } from "../git.ts";
5
6
 
@@ -11,6 +12,7 @@ export interface SearchableModelSelectorOptions {
11
12
  allOptions: string[];
12
13
  currentOption?: string;
13
14
  keybindings: KeybindingsManager;
15
+ multiSelect?: false;
14
16
  onCancel: () => void;
15
17
  onSelect: (value: string) => void;
16
18
  theme: Theme;
@@ -18,6 +20,15 @@ export interface SearchableModelSelectorOptions {
18
20
  tui: RenderRequester;
19
21
  }
20
22
 
23
+ export interface SearchableModelMultiSelectorOptions extends Omit<
24
+ SearchableModelSelectorOptions,
25
+ "currentOption" | "multiSelect" | "onSelect"
26
+ > {
27
+ currentOptions: string[];
28
+ multiSelect: true;
29
+ onSelect: (values: string[]) => void;
30
+ }
31
+
21
32
  export interface ManualAdvisorRequest {
22
33
  /** The repository disclosure level selected for this consultation. */
23
34
  gitContext: GitContextLevel;
@@ -82,6 +93,7 @@ export interface AdvisorSettings {
82
93
  jevTurnGateNoulThreshold?: number;
83
94
  loopThreshold?: number;
84
95
  maxCallsPerSession?: number;
96
+ modelWhitelist?: string[];
85
97
  outcomeLogging?: boolean;
86
98
  planGate: boolean;
87
99
  redactSecrets?: boolean;
@@ -100,6 +112,8 @@ export interface AdvisorSettings {
100
112
  export interface AdvisorSettingsSelectorOptions {
101
113
  effortLevels: string[];
102
114
  initial: AdvisorSettings;
115
+ keybindings?: KeybindingsManager;
116
+ modelRefs?: string[];
103
117
  onCancel: () => void;
104
118
  onChange?: (settings: AdvisorSettings) => void;
105
119
  /** @deprecated Use onChange; retained for extensions embedding this component. */
package/src/ui.ts CHANGED
@@ -1,4 +1,3 @@
1
- // biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public UI module contract.
2
1
  export { ManualAdvisorDialog } from "./ui/manual-dialog.ts";
3
2
  export { SearchableModelSelector } from "./ui/model-selector.ts";
4
3
  export { AdvisorSettingsSelector } from "./ui/settings-selector.ts";
package/src/usage.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { Usage } from "@earendil-works/pi-ai/compat";
2
2
 
3
+ import { isNumber, isRecordOf } from "./content-utils.ts";
4
+ import type { JsonValue } from "./content-utils.ts";
5
+
3
6
  /** Normalized usage returned by an Advisor or Scout provider response. */
4
7
  export interface AdvisorUsageSnapshot {
5
8
  cacheRead?: number;
@@ -22,26 +25,22 @@ export interface AdvisorUsageTotals {
22
25
  totalTokens?: number;
23
26
  }
24
27
 
25
- const finite = (value: unknown) =>
26
- typeof value === "number" && Number.isFinite(value) && value >= 0
27
- ? value
28
- : undefined;
28
+ const finite = (value: JsonValue | undefined): number | undefined =>
29
+ isNumber(value) && Number.isFinite(value) && value >= 0 ? value : undefined;
29
30
 
30
31
  const add = (left: number | undefined, right: number | undefined) =>
31
32
  left === undefined || right === undefined ? (left ?? right) : left + right;
32
33
 
33
- import { isRecord } from "./content-utils.ts";
34
-
35
34
  const costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
36
35
 
37
36
  /** Extracts provider-agnostic usage fields without trusting provider metadata. */
38
- export const snapshotAdvisorUsage = (
39
- usage: unknown
37
+ export const snapshotAdvisorUsage = <Input>(
38
+ usage: Input
40
39
  ): AdvisorUsageSnapshot | undefined => {
41
- if (!isRecord(usage)) {
40
+ if (!isRecordOf(usage)) {
42
41
  return undefined;
43
42
  }
44
- const cost = isRecord(usage.cost) ? usage.cost : undefined;
43
+ const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
45
44
  const snapshot = {
46
45
  cacheRead: finite(usage.cacheRead),
47
46
  cacheWrite: finite(usage.cacheWrite),
@@ -60,7 +59,7 @@ export const snapshotAdvisorUsage = (
60
59
  };
61
60
 
62
61
  /** Returns the reported provider cost, when the response includes one. */
63
- export const advisorUsageCost = (usage: unknown): number | undefined =>
62
+ export const advisorUsageCost = <Input>(usage: Input): number | undefined =>
64
63
  snapshotAdvisorUsage(usage)?.cost;
65
64
 
66
65
  /**
@@ -68,23 +67,21 @@ export const advisorUsageCost = (usage: unknown): number | undefined =>
68
67
  * Missing fields become zero only at this Pi API boundary; absent usage remains
69
68
  * undefined so an unavailable request is never presented as a zero-cost call.
70
69
  */
71
- export const advisorUsageForPi = (usage: unknown): Usage | undefined => {
70
+ export const advisorUsageForPi = <Input>(usage: Input): Usage | undefined => {
72
71
  const snapshot = snapshotAdvisorUsage(usage);
73
- if (!(snapshot && isRecord(usage))) {
72
+ if (!(snapshot && isRecordOf(usage))) {
74
73
  return undefined;
75
74
  }
76
- const cost = isRecord(usage.cost) ? usage.cost : undefined;
75
+ const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
77
76
  const input = snapshot.input ?? 0;
78
77
  const output = snapshot.output ?? 0;
79
78
  const cacheRead = snapshot.cacheRead ?? 0;
80
79
  const cacheWrite = snapshot.cacheWrite ?? 0;
81
80
  const cacheWrite1h = finite(usage.cacheWrite1h);
82
81
  const reasoning = finite(usage.reasoning);
83
- return {
82
+ const piUsage: Usage = {
84
83
  cacheRead,
85
84
  cacheWrite,
86
- ...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
87
- ...(reasoning === undefined ? {} : { reasoning }),
88
85
  cost: {
89
86
  cacheRead: finite(cost?.cacheRead) ?? 0,
90
87
  cacheWrite: finite(cost?.cacheWrite) ?? 0,
@@ -97,6 +94,13 @@ export const advisorUsageForPi = (usage: unknown): Usage | undefined => {
97
94
  totalTokens:
98
95
  snapshot.totalTokens ?? input + output + cacheRead + cacheWrite,
99
96
  };
97
+ if (cacheWrite1h !== undefined) {
98
+ piUsage.cacheWrite1h = cacheWrite1h;
99
+ }
100
+ if (reasoning !== undefined) {
101
+ piUsage.reasoning = reasoning;
102
+ }
103
+ return piUsage;
100
104
  };
101
105
 
102
106
  /** Creates empty totals without treating absent usage as zero usage. */
@@ -107,7 +111,10 @@ export const emptyAdvisorUsageTotals = (): AdvisorUsageTotals => ({
107
111
  });
108
112
 
109
113
  /** Adds one direct Advisor response to session-local usage totals. */
110
- export const addAdvisorUsage = (totals: AdvisorUsageTotals, usage: unknown) => {
114
+ export const addAdvisorUsage = <Input>(
115
+ totals: AdvisorUsageTotals,
116
+ usage: Input
117
+ ) => {
111
118
  totals.calls += 1;
112
119
  const snapshot = snapshotAdvisorUsage(usage);
113
120
  if (!snapshot) {
@@ -165,7 +172,7 @@ const formatUsageFields = (usage: AdvisorUsageSnapshot): string | undefined => {
165
172
  return tokens.join(" · ") || undefined;
166
173
  };
167
174
 
168
- export const formatAdvisorUsage = (usage: unknown): string | undefined => {
175
+ export const formatAdvisorUsage = <Input>(usage: Input): string | undefined => {
169
176
  const snapshot = snapshotAdvisorUsage(usage);
170
177
  return snapshot ? formatUsageFields(snapshot) : undefined;
171
178
  };