pi-advisor-flow 0.9.2-dev.2 → 0.9.2-dev.4
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/index.js +101 -34
- package/package.json +8 -12
- package/src/commands/runtime.ts +2 -3
- package/src/commands/settings-persistence.ts +14 -7
- package/src/config/args.ts +3 -1
- package/src/config/storage.ts +23 -2
- package/src/image-attachments.ts +7 -3
- package/src/preferences.ts +13 -1
- package/src/redaction.ts +1 -1
- package/src/tools/jev-filter.ts +2 -1
- package/src/tools/registration.ts +2 -2
- package/src/tools/render-advisor-result.ts +18 -11
- package/src/tools/session.ts +14 -0
- package/src/ui/manual-dialog.ts +10 -3
- package/src/usage.ts +34 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
|
|
6
6
|
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Prevented settings saves from overwriting malformed `advisor.json` files and rolled back runtime settings when persistence fails.
|
|
12
|
+
- Kept trusted project preferences inside the trusted project during file replacement races and isolated Advisor safety state between sessions.
|
|
13
|
+
- Stopped discarded streaming renders from retaining repaint timers, made failed manual submissions retryable, and bounded unterminated-secret redaction.
|
|
14
|
+
- Preserved `=` in model arguments, enforced UTF-8 byte caps on reattached advice, deduplicated withheld image paths, and avoided false zero-cost totals for incomplete provider usage.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Pi 0.87.x and 0.99.x hosts are no longer supported; Pi 1.0.x is now the supported Pi host and the peer range is `^1.0.0`.
|
|
19
|
+
- Updated the development toolchain to the Pi 1.0.0 packages.
|
|
20
|
+
|
|
7
21
|
## 0.9.1 - 2026-09-30
|
|
8
22
|
|
|
9
23
|
### Fixed
|
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -826,7 +826,9 @@ var parseArgs = (args) => {
|
|
|
826
826
|
let nextAdvisor = advisorRef;
|
|
827
827
|
let nextContextMaxChars = contextMaxCharsRef;
|
|
828
828
|
for (const token of args.trim().split(ARGUMENT_WHITESPACE).filter(Boolean)) {
|
|
829
|
-
const
|
|
829
|
+
const separator = token.indexOf("=");
|
|
830
|
+
const key = separator === -1 ? token : token.slice(0, separator);
|
|
831
|
+
const value = separator === -1 ? undefined : token.slice(separator + 1);
|
|
830
832
|
if (key === "executor" && value) {
|
|
831
833
|
nextExecutor = value;
|
|
832
834
|
}
|
|
@@ -971,6 +973,21 @@ var readExistingConfig = (path) => {
|
|
|
971
973
|
return {};
|
|
972
974
|
}
|
|
973
975
|
};
|
|
976
|
+
var readConfigForSave = (path) => {
|
|
977
|
+
if (!existsSync(path)) {
|
|
978
|
+
return {};
|
|
979
|
+
}
|
|
980
|
+
let parsed;
|
|
981
|
+
try {
|
|
982
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
983
|
+
} catch (error) {
|
|
984
|
+
throw new Error(`Cannot save Advisor configuration at ${path}: existing JSON is malformed; fix advisor.json before saving.`, { cause: error });
|
|
985
|
+
}
|
|
986
|
+
if (!isRecord(parsed)) {
|
|
987
|
+
throw new Error(`Cannot save Advisor configuration at ${path}: configuration must be a JSON object.`);
|
|
988
|
+
}
|
|
989
|
+
return parsed;
|
|
990
|
+
};
|
|
974
991
|
var shouldPersistConfigKey = (key, persistAdvisor, persistExecutor) => (key !== "advisor" || persistAdvisor) && (key !== "executor" || persistExecutor);
|
|
975
992
|
var applyChangedConfigValues = (existing, current, changedKeys, persistAdvisor, persistExecutor) => {
|
|
976
993
|
const dropped = new Set;
|
|
@@ -1052,13 +1069,13 @@ var saveConfig = (_ctx, options = {}) => {
|
|
|
1052
1069
|
const path = join(getAgentDir(), "advisor.json");
|
|
1053
1070
|
const persistAdvisor = options.persistAdvisor ?? true;
|
|
1054
1071
|
const persistExecutor = options.persistExecutor ?? true;
|
|
1055
|
-
const existing = readExistingConfig(path);
|
|
1056
1072
|
const current = currentConfigState();
|
|
1057
1073
|
const baseline = loadedConfigPath === path ? loadedConfigState : undefined;
|
|
1058
1074
|
const changedKeys = baseline ? SAVED_CONFIG_KEYS.filter((key) => !sameConfigValue(current[key], baseline[key])) : [...SAVED_CONFIG_KEYS];
|
|
1059
1075
|
if (changedKeys.length === 0) {
|
|
1060
1076
|
return path;
|
|
1061
1077
|
}
|
|
1078
|
+
const existing = readConfigForSave(path);
|
|
1062
1079
|
const data = applyChangedConfigValues(existing, current, changedKeys, persistAdvisor, persistExecutor);
|
|
1063
1080
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}
|
|
1064
1081
|
`);
|
|
@@ -1076,7 +1093,7 @@ var saveConfig = (_ctx, options = {}) => {
|
|
|
1076
1093
|
};
|
|
1077
1094
|
var saveGlobalOutcomeLogging = (enabled) => {
|
|
1078
1095
|
const path = join(getAgentDir(), "advisor.json");
|
|
1079
|
-
const existing =
|
|
1096
|
+
const existing = readConfigForSave(path);
|
|
1080
1097
|
writeFileSync(path, `${JSON.stringify({ ...existing, advisorOutcomeLogging: enabled }, null, 2)}
|
|
1081
1098
|
`);
|
|
1082
1099
|
resetConfigCache();
|
|
@@ -1505,7 +1522,7 @@ var REDACTION_MARKER = "[REDACTED SECRET]";
|
|
|
1505
1522
|
var PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/giu;
|
|
1506
1523
|
var PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/iu;
|
|
1507
1524
|
var CREDENTIAL_NAME = String.raw`(?:api[_-]?key|token|secret|password|passwd)`;
|
|
1508
|
-
var SECRET_ASSIGNMENT_PATTERN = new RegExp(String.raw`(?:(?:"(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}"|'(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}')|(?:\b|_)${CREDENTIAL_NAME})\
|
|
1525
|
+
var SECRET_ASSIGNMENT_PATTERN = new RegExp(String.raw`(?:(?:"(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}"|'(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}')|(?:\b|_)${CREDENTIAL_NAME})[ \t]*[:=][ \t]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|"(?:\\.|[^"\r\n])*(?=\r?\n|$)|'(?:\\.|[^'\r\n])*(?=\r?\n|$)|[^\s"'&,;)}\]]+)`, "giu");
|
|
1509
1526
|
var SECRET_PATTERNS = [
|
|
1510
1527
|
/-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----[\s\S]*?-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/giu,
|
|
1511
1528
|
/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/giu,
|
|
@@ -2408,7 +2425,9 @@ var readImageAttachment = async (root, name, mimeType, remaining) => {
|
|
|
2408
2425
|
}
|
|
2409
2426
|
};
|
|
2410
2427
|
var readImageFiles = async (cwd, requested, enabled, kind, remainingBytes, remainingCount) => {
|
|
2411
|
-
const names =
|
|
2428
|
+
const names = [
|
|
2429
|
+
...new Set(Array.isArray(requested) ? requested.filter((path) => isString(path) && imageFileCandidate(path)) : [])
|
|
2430
|
+
];
|
|
2412
2431
|
if (!enabled || !names.length) {
|
|
2413
2432
|
return { images: [], omitted: names.length };
|
|
2414
2433
|
}
|
|
@@ -2445,6 +2464,7 @@ var readImageFiles = async (cwd, requested, enabled, kind, remainingBytes, remai
|
|
|
2445
2464
|
};
|
|
2446
2465
|
|
|
2447
2466
|
// src/preferences.ts
|
|
2467
|
+
import { constants as constants3 } from "node:fs";
|
|
2448
2468
|
import { lstat as lstat3, open as open3, realpath as realpath3 } from "node:fs/promises";
|
|
2449
2469
|
import { join as join2, relative as relative2 } from "node:path";
|
|
2450
2470
|
var PREFERENCES_MAX_BYTES = 8 * 1024;
|
|
@@ -2468,8 +2488,13 @@ var readProjectPreferences = async (ctx, maxBytes = PREFERENCES_MAX_BYTES, redac
|
|
|
2468
2488
|
if (!inside(root, resolved)) {
|
|
2469
2489
|
return;
|
|
2470
2490
|
}
|
|
2471
|
-
const
|
|
2491
|
+
const flags = constants3.O_NOFOLLOW ? constants3.O_RDONLY + constants3.O_NOFOLLOW : constants3.O_RDONLY;
|
|
2492
|
+
const file = await open3(resolved, flags);
|
|
2472
2493
|
try {
|
|
2494
|
+
const opened = await file.stat();
|
|
2495
|
+
if (!opened.isFile() || opened.dev !== stats.dev || opened.ino !== stats.ino) {
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2473
2498
|
const buffer = Buffer.alloc(maxBytes + 1);
|
|
2474
2499
|
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
2475
2500
|
const source = buffer.subarray(0, bytesRead).toString("utf-8");
|
|
@@ -3100,6 +3125,10 @@ var buildScoutManifest = (ctx, options = {}) => {
|
|
|
3100
3125
|
var finite = (value) => isNumber(value) && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
3101
3126
|
var add = (left, right) => left === undefined || right === undefined ? left ?? right : left + right;
|
|
3102
3127
|
var costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
|
|
3128
|
+
var hasPartialProviderCost = (snapshot, usage) => {
|
|
3129
|
+
const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
|
|
3130
|
+
return cost !== undefined && snapshot.cost === undefined && costFields.some((field) => finite(cost[field]) !== undefined);
|
|
3131
|
+
};
|
|
3103
3132
|
var snapshotAdvisorUsage = (usage) => {
|
|
3104
3133
|
if (!isRecordOf(usage)) {
|
|
3105
3134
|
return;
|
|
@@ -3117,18 +3146,29 @@ var snapshotAdvisorUsage = (usage) => {
|
|
|
3117
3146
|
return Object.values(snapshot).some((value) => value !== undefined) || hasCostField ? snapshot : undefined;
|
|
3118
3147
|
};
|
|
3119
3148
|
var advisorUsageCost = (usage) => snapshotAdvisorUsage(usage)?.cost;
|
|
3120
|
-
var
|
|
3149
|
+
var completeUsageSnapshot = (usage) => {
|
|
3121
3150
|
const snapshot = snapshotAdvisorUsage(usage);
|
|
3122
3151
|
if (!(snapshot && isRecordOf(usage))) {
|
|
3123
3152
|
return;
|
|
3124
3153
|
}
|
|
3125
|
-
|
|
3154
|
+
if (hasPartialProviderCost(snapshot, usage)) {
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
return { snapshot, usage };
|
|
3158
|
+
};
|
|
3159
|
+
var advisorUsageForPi = (usage) => {
|
|
3160
|
+
const complete = completeUsageSnapshot(usage);
|
|
3161
|
+
if (!complete) {
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
const { snapshot } = complete;
|
|
3165
|
+
const cost = isRecordOf(complete.usage.cost) ? complete.usage.cost : undefined;
|
|
3126
3166
|
const input = snapshot.input ?? 0;
|
|
3127
3167
|
const output = snapshot.output ?? 0;
|
|
3128
3168
|
const cacheRead = snapshot.cacheRead ?? 0;
|
|
3129
3169
|
const cacheWrite = snapshot.cacheWrite ?? 0;
|
|
3130
|
-
const cacheWrite1h = finite(usage.cacheWrite1h);
|
|
3131
|
-
const reasoning = finite(usage.reasoning);
|
|
3170
|
+
const cacheWrite1h = finite(complete.usage.cacheWrite1h);
|
|
3171
|
+
const reasoning = finite(complete.usage.reasoning);
|
|
3132
3172
|
const piUsage = {
|
|
3133
3173
|
cacheRead,
|
|
3134
3174
|
cacheWrite,
|
|
@@ -4659,6 +4699,16 @@ class AdvisorSessionState {
|
|
|
4659
4699
|
|
|
4660
4700
|
// src/tools/session.ts
|
|
4661
4701
|
var advisorSessionState = new AdvisorSessionState;
|
|
4702
|
+
var sessionStates = new WeakMap;
|
|
4703
|
+
var sessionStateFor = (owner) => {
|
|
4704
|
+
const existing = sessionStates.get(owner);
|
|
4705
|
+
if (existing) {
|
|
4706
|
+
return existing;
|
|
4707
|
+
}
|
|
4708
|
+
const state = new AdvisorSessionState;
|
|
4709
|
+
sessionStates.set(owner, state);
|
|
4710
|
+
return state;
|
|
4711
|
+
};
|
|
4662
4712
|
|
|
4663
4713
|
// src/commands/runtime.ts
|
|
4664
4714
|
var notify = (ctx, message, level) => {
|
|
@@ -4690,7 +4740,7 @@ class CommandRuntime {
|
|
|
4690
4740
|
lastSameModelDisabled;
|
|
4691
4741
|
constructor(pi, dependencies = {}) {
|
|
4692
4742
|
this.pi = pi;
|
|
4693
|
-
this.advisorSessionState = dependencies.sessionState ??
|
|
4743
|
+
this.advisorSessionState = dependencies.sessionState ?? sessionStateFor(pi);
|
|
4694
4744
|
this.herdrActivity = dependencies.herdrActivity ?? herdrAdvisorActivity;
|
|
4695
4745
|
this.scoutStatus = dependencies.statusManager ?? new ScoutStatusManager(false);
|
|
4696
4746
|
this.requestAdvisor = dependencies.consult ?? ((ctx, question, signal, onChunk, onScout, gitContext) => consultAdvisor(ctx, question, signal, onChunk, "manual", gitContext, undefined, undefined, undefined, onScout));
|
|
@@ -5032,7 +5082,8 @@ class ManualAdvisorDialog {
|
|
|
5032
5082
|
focusTarget = "editor";
|
|
5033
5083
|
state = {
|
|
5034
5084
|
completed: false,
|
|
5035
|
-
focused: false
|
|
5085
|
+
focused: false,
|
|
5086
|
+
submitting: false
|
|
5036
5087
|
};
|
|
5037
5088
|
get focused() {
|
|
5038
5089
|
return this.state.focused;
|
|
@@ -5204,17 +5255,22 @@ class ManualAdvisorDialog {
|
|
|
5204
5255
|
return this.options.keybindings.matches(keyData, action) || matchesKey(keyData, fallback);
|
|
5205
5256
|
}
|
|
5206
5257
|
submit(message = this.editor.getText()) {
|
|
5207
|
-
if (this.state.completed) {
|
|
5258
|
+
if (this.state.completed || this.state.submitting) {
|
|
5208
5259
|
return;
|
|
5209
5260
|
}
|
|
5210
|
-
this.state.
|
|
5261
|
+
this.state.submitting = true;
|
|
5211
5262
|
const request = {
|
|
5212
5263
|
gitContext: this.gitLevels[this.gitIndex] ?? "off"
|
|
5213
5264
|
};
|
|
5214
5265
|
if (message) {
|
|
5215
5266
|
request.message = message;
|
|
5216
5267
|
}
|
|
5217
|
-
|
|
5268
|
+
try {
|
|
5269
|
+
this.options.onSubmit(request);
|
|
5270
|
+
this.state.completed = true;
|
|
5271
|
+
} finally {
|
|
5272
|
+
this.state.submitting = false;
|
|
5273
|
+
}
|
|
5218
5274
|
}
|
|
5219
5275
|
cancel() {
|
|
5220
5276
|
if (this.state.completed) {
|
|
@@ -5855,7 +5911,7 @@ var screenConsultation = (ctx, session, options, deps = {}) => {
|
|
|
5855
5911
|
decision: "skip",
|
|
5856
5912
|
kind: "repeat",
|
|
5857
5913
|
reason: "already answered earlier in this session",
|
|
5858
|
-
reattachedAdvice: reattached
|
|
5914
|
+
reattachedAdvice: capUtf8Bytes(reattached, REATTACHED_ADVICE_CAP_BYTES)
|
|
5859
5915
|
});
|
|
5860
5916
|
}
|
|
5861
5917
|
if (!advisorJevFilterEnabledRef) {
|
|
@@ -7368,13 +7424,19 @@ var applyAdvisorSettings = (settings) => {
|
|
|
7368
7424
|
applyDisclosureSettings(settings);
|
|
7369
7425
|
};
|
|
7370
7426
|
var saveAdvisorSettings = (ctx, settings) => {
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7427
|
+
const previous = getAdvisorSettings();
|
|
7428
|
+
try {
|
|
7429
|
+
applyAdvisorSettings(settings);
|
|
7430
|
+
const persisted = getPersistedModelRefs();
|
|
7431
|
+
saveConfig(ctx, {
|
|
7432
|
+
persistAdvisor: Boolean(persisted.advisor),
|
|
7433
|
+
persistExecutor: Boolean(persisted.executor)
|
|
7434
|
+
});
|
|
7435
|
+
saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
|
|
7436
|
+
} catch (error) {
|
|
7437
|
+
applyAdvisorSettings(previous);
|
|
7438
|
+
throw error;
|
|
7439
|
+
}
|
|
7378
7440
|
};
|
|
7379
7441
|
|
|
7380
7442
|
// src/commands/settings-commands.ts
|
|
@@ -7753,11 +7815,23 @@ import { Box as Box3, Markdown as Markdown4, Spacer, Text as Text5 } from "@eare
|
|
|
7753
7815
|
var advisorResultDetails = (result) => result.details;
|
|
7754
7816
|
var syncRenderPhase = (context, phase) => {
|
|
7755
7817
|
if (context.state.phase !== phase && context.state.timerId) {
|
|
7756
|
-
|
|
7818
|
+
clearTimeout(context.state.timerId);
|
|
7757
7819
|
context.state.timerId = undefined;
|
|
7758
7820
|
}
|
|
7759
7821
|
context.state.phase = phase;
|
|
7760
7822
|
};
|
|
7823
|
+
var scheduleRender = (context) => {
|
|
7824
|
+
const timer = setTimeout(() => {
|
|
7825
|
+
if (context.state.timerId !== timer) {
|
|
7826
|
+
return;
|
|
7827
|
+
}
|
|
7828
|
+
context.state.timerId = undefined;
|
|
7829
|
+
try {
|
|
7830
|
+
context.invalidate();
|
|
7831
|
+
} catch {}
|
|
7832
|
+
}, 80);
|
|
7833
|
+
context.state.timerId = timer;
|
|
7834
|
+
};
|
|
7761
7835
|
var formatByteSize = (bytes) => {
|
|
7762
7836
|
if (bytes >= 1024 ** 3) {
|
|
7763
7837
|
return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
@@ -7796,14 +7870,7 @@ var renderPartialAdvisorResult = (box, result, expanded, theme, context) => {
|
|
|
7796
7870
|
const scoutActive = scout?.status === "calling" || scout?.status === "streaming";
|
|
7797
7871
|
syncRenderPhase(context, scoutActive ? "scout" : "advisor");
|
|
7798
7872
|
if (!context.state.timerId) {
|
|
7799
|
-
context
|
|
7800
|
-
try {
|
|
7801
|
-
context.invalidate();
|
|
7802
|
-
} catch {
|
|
7803
|
-
clearInterval(context.state.timerId);
|
|
7804
|
-
context.state.timerId = undefined;
|
|
7805
|
-
}
|
|
7806
|
-
}, 80);
|
|
7873
|
+
scheduleRender(context);
|
|
7807
7874
|
}
|
|
7808
7875
|
if (scout) {
|
|
7809
7876
|
renderScoutDetails(box, scout, expanded, theme);
|
|
@@ -7846,7 +7913,7 @@ var finalResultLines = (details, advice, theme) => {
|
|
|
7846
7913
|
var renderFinalAdvisorResult = (box, result, expanded, theme, context) => {
|
|
7847
7914
|
syncRenderPhase(context, "final");
|
|
7848
7915
|
if (context.state.timerId) {
|
|
7849
|
-
|
|
7916
|
+
clearTimeout(context.state.timerId);
|
|
7850
7917
|
context.state.timerId = undefined;
|
|
7851
7918
|
}
|
|
7852
7919
|
const details = advisorResultDetails(result);
|
|
@@ -8487,7 +8554,7 @@ var registerToolRenderers = (pi) => {
|
|
|
8487
8554
|
};
|
|
8488
8555
|
|
|
8489
8556
|
// src/tools/registration.ts
|
|
8490
|
-
var registerAdvisorTool = (pi, session =
|
|
8557
|
+
var registerAdvisorTool = (pi, session = sessionStateFor(pi), dependencies = {}) => {
|
|
8491
8558
|
const registration = {
|
|
8492
8559
|
appendOutcome: dependencies.appendOutcome ?? appendOutcome,
|
|
8493
8560
|
consult: dependencies.consult ?? consultAdvisor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.9.2-dev.
|
|
3
|
+
"version": "0.9.2-dev.4",
|
|
4
4
|
"description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"advisor",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"format:check": "oxfmt --check"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-ai": "0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
56
|
-
"@earendil-works/pi-tui": "0.
|
|
54
|
+
"@earendil-works/pi-ai": "1.0.0",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
56
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
57
57
|
"@oxlint/plugins": "1.86.0",
|
|
58
58
|
"@types/node": "^26.6.3",
|
|
59
59
|
"@typesafe-ai/sdk": "^0.6.0",
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"ultracite": "7.12.2"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@earendil-works/pi-ai": "^0.
|
|
72
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
73
|
-
"@earendil-works/pi-tui": "^0.
|
|
71
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
73
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
74
74
|
"typebox": "^1.1.38"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|
|
@@ -100,11 +100,7 @@
|
|
|
100
100
|
"lint-staged": {
|
|
101
101
|
"*.{ts,tsx}": [
|
|
102
102
|
"oxfmt",
|
|
103
|
-
"oxlint --fix"
|
|
104
|
-
],
|
|
105
|
-
"{src,test,scripts,tools,extensions}/**/*.{ts,tsx}": [
|
|
106
|
-
"oxfmt",
|
|
107
|
-
"oxlint --fix"
|
|
103
|
+
"oxlint --fix --no-error-on-unmatched-pattern"
|
|
108
104
|
],
|
|
109
105
|
"*.{json,jsonc}": [
|
|
110
106
|
"oxfmt"
|
package/src/commands/runtime.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
sameModelAdvisorNotice,
|
|
13
13
|
} from "../tools/model-access.ts";
|
|
14
14
|
import { ScoutStatusManager } from "../tools/scout-status.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { sessionStateFor } from "../tools/session.ts";
|
|
16
16
|
import { uiAction } from "../ui-guard.ts";
|
|
17
17
|
import type {
|
|
18
18
|
CommandDependencies,
|
|
@@ -60,8 +60,7 @@ class CommandRuntime implements CommandRuntimeContract {
|
|
|
60
60
|
|
|
61
61
|
constructor(pi: ExtensionAPI, dependencies: CommandDependencies = {}) {
|
|
62
62
|
this.pi = pi;
|
|
63
|
-
this.advisorSessionState =
|
|
64
|
-
dependencies.sessionState ?? defaultAdvisorSessionState;
|
|
63
|
+
this.advisorSessionState = dependencies.sessionState ?? sessionStateFor(pi);
|
|
65
64
|
this.herdrActivity = dependencies.herdrActivity ?? herdrAdvisorActivity;
|
|
66
65
|
this.scoutStatus =
|
|
67
66
|
dependencies.statusManager ?? new ScoutStatusManager(false);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
getAdvisorSettings,
|
|
4
5
|
getPersistedModelRefs,
|
|
5
6
|
setAdvisorAutoLoopGateRef,
|
|
6
7
|
setAdvisorBlockOnBlockedRef,
|
|
@@ -140,11 +141,17 @@ export const saveAdvisorSettings = (
|
|
|
140
141
|
ctx: ExtensionContext,
|
|
141
142
|
settings: AdvisorSettings
|
|
142
143
|
) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
const previous = getAdvisorSettings();
|
|
145
|
+
try {
|
|
146
|
+
applyAdvisorSettings(settings);
|
|
147
|
+
const persisted = getPersistedModelRefs();
|
|
148
|
+
saveConfig(ctx, {
|
|
149
|
+
persistAdvisor: Boolean(persisted.advisor),
|
|
150
|
+
persistExecutor: Boolean(persisted.executor),
|
|
151
|
+
});
|
|
152
|
+
saveGlobalOutcomeLogging(settings.outcomeLogging ?? false);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
applyAdvisorSettings(previous);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
150
157
|
};
|
package/src/config/args.ts
CHANGED
|
@@ -16,7 +16,9 @@ export const parseArgs = (args: string): string | undefined => {
|
|
|
16
16
|
let nextAdvisor = advisorRef;
|
|
17
17
|
let nextContextMaxChars = contextMaxCharsRef;
|
|
18
18
|
for (const token of args.trim().split(ARGUMENT_WHITESPACE).filter(Boolean)) {
|
|
19
|
-
const
|
|
19
|
+
const separator = token.indexOf("=");
|
|
20
|
+
const key = separator === -1 ? token : token.slice(0, separator);
|
|
21
|
+
const value = separator === -1 ? undefined : token.slice(separator + 1);
|
|
20
22
|
if (key === "executor" && value) {
|
|
21
23
|
nextExecutor = value;
|
|
22
24
|
}
|
package/src/config/storage.ts
CHANGED
|
@@ -52,6 +52,27 @@ export const readExistingConfig = (path: string): RecordValue => {
|
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
const readConfigForSave = (path: string): RecordValue => {
|
|
56
|
+
if (!existsSync(path)) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
let parsed: unknown;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Cannot save Advisor configuration at ${path}: existing JSON is malformed; fix advisor.json before saving.`,
|
|
65
|
+
{ cause: error }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!isRecord(parsed)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Cannot save Advisor configuration at ${path}: configuration must be a JSON object.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
};
|
|
75
|
+
|
|
55
76
|
const shouldPersistConfigKey = (
|
|
56
77
|
key: SavedConfigKey,
|
|
57
78
|
persistAdvisor: boolean,
|
|
@@ -197,7 +218,6 @@ export const saveConfig = (
|
|
|
197
218
|
const path = join(getAgentDir(), "advisor.json");
|
|
198
219
|
const persistAdvisor = options.persistAdvisor ?? true;
|
|
199
220
|
const persistExecutor = options.persistExecutor ?? true;
|
|
200
|
-
const existing = readExistingConfig(path);
|
|
201
221
|
const current = currentConfigState();
|
|
202
222
|
const baseline = loadedConfigPath === path ? loadedConfigState : undefined;
|
|
203
223
|
const changedKeys = baseline
|
|
@@ -208,6 +228,7 @@ export const saveConfig = (
|
|
|
208
228
|
if (changedKeys.length === 0) {
|
|
209
229
|
return path;
|
|
210
230
|
}
|
|
231
|
+
const existing = readConfigForSave(path);
|
|
211
232
|
const data = applyChangedConfigValues(
|
|
212
233
|
existing,
|
|
213
234
|
current,
|
|
@@ -240,7 +261,7 @@ export const saveConfig = (
|
|
|
240
261
|
/** Outcome logging is deliberately written only to the global Pi configuration. */
|
|
241
262
|
export const saveGlobalOutcomeLogging = (enabled: boolean) => {
|
|
242
263
|
const path = join(getAgentDir(), "advisor.json");
|
|
243
|
-
const existing =
|
|
264
|
+
const existing = readConfigForSave(path);
|
|
244
265
|
writeFileSync(
|
|
245
266
|
path,
|
|
246
267
|
`${JSON.stringify({ ...existing, advisorOutcomeLogging: enabled }, null, 2)}\n`
|
package/src/image-attachments.ts
CHANGED
|
@@ -72,9 +72,13 @@ export const readImageFiles = async (
|
|
|
72
72
|
remainingBytes: number,
|
|
73
73
|
remainingCount: number
|
|
74
74
|
): Promise<{ images: ImageAttachment[]; omitted: number }> => {
|
|
75
|
-
const names =
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
const names = [
|
|
76
|
+
...new Set(
|
|
77
|
+
Array.isArray(requested)
|
|
78
|
+
? requested.filter((path) => isString(path) && imageFileCandidate(path))
|
|
79
|
+
: []
|
|
80
|
+
),
|
|
81
|
+
];
|
|
78
82
|
if (!enabled || !names.length) {
|
|
79
83
|
return { images: [], omitted: names.length };
|
|
80
84
|
}
|
package/src/preferences.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
1
2
|
import { lstat, open, realpath } from "node:fs/promises";
|
|
2
3
|
import { join, relative } from "node:path";
|
|
3
4
|
|
|
@@ -38,8 +39,19 @@ export const readProjectPreferences = async (
|
|
|
38
39
|
if (!inside(root, resolved)) {
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
|
-
const
|
|
42
|
+
const flags = constants.O_NOFOLLOW
|
|
43
|
+
? constants.O_RDONLY + constants.O_NOFOLLOW
|
|
44
|
+
: constants.O_RDONLY;
|
|
45
|
+
const file = await open(resolved, flags);
|
|
42
46
|
try {
|
|
47
|
+
const opened = await file.stat();
|
|
48
|
+
if (
|
|
49
|
+
!opened.isFile() ||
|
|
50
|
+
opened.dev !== stats.dev ||
|
|
51
|
+
opened.ino !== stats.ino
|
|
52
|
+
) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
43
55
|
const buffer = Buffer.alloc(maxBytes + 1);
|
|
44
56
|
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
45
57
|
const source = buffer.subarray(0, bytesRead).toString("utf-8");
|
package/src/redaction.ts
CHANGED
|
@@ -5,7 +5,7 @@ const PEM_BEGIN_PATTERN = /-----BEGIN(?: [A-Z0-9]+)? PRIVATE KEY-----/giu;
|
|
|
5
5
|
const PEM_END_PATTERN = /-----END(?: [A-Z0-9]+)? PRIVATE KEY-----/iu;
|
|
6
6
|
const CREDENTIAL_NAME = String.raw`(?:api[_-]?key|token|secret|password|passwd)`;
|
|
7
7
|
const SECRET_ASSIGNMENT_PATTERN = new RegExp(
|
|
8
|
-
String.raw`(?:(?:"(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}"|'(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}')|(?:\b|_)${CREDENTIAL_NAME})\
|
|
8
|
+
String.raw`(?:(?:"(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}"|'(?:[a-z0-9]+[_-])*?${CREDENTIAL_NAME}')|(?:\b|_)${CREDENTIAL_NAME})[ \t]*[:=][ \t]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|"(?:\\.|[^"\r\n])*(?=\r?\n|$)|'(?:\\.|[^'\r\n])*(?=\r?\n|$)|[^\s"'&,;)}\]]+)`,
|
|
9
9
|
"giu"
|
|
10
10
|
);
|
|
11
11
|
const SECRET_PATTERNS = [
|
package/src/tools/jev-filter.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
advisorJevFilterSkipConfidenceRef,
|
|
9
9
|
isSimpleMode,
|
|
10
10
|
} from "../config/state.ts";
|
|
11
|
+
import { capUtf8Bytes } from "../content-utils.ts";
|
|
11
12
|
import { JevFailure, jevClientFromCredentials } from "../jev/client.ts";
|
|
12
13
|
import { consumePlaintextKeyWarning } from "../jev/key-store.ts";
|
|
13
14
|
import {
|
|
@@ -187,7 +188,7 @@ export const screenConsultation = (
|
|
|
187
188
|
decision: "skip",
|
|
188
189
|
kind: "repeat",
|
|
189
190
|
reason: "already answered earlier in this session",
|
|
190
|
-
reattachedAdvice: reattached
|
|
191
|
+
reattachedAdvice: capUtf8Bytes(reattached, REATTACHED_ADVICE_CAP_BYTES),
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
194
|
if (!advisorJevFilterEnabledRef) {
|
|
@@ -12,7 +12,7 @@ import { registerToolLifecycle } from "./register-lifecycle.ts";
|
|
|
12
12
|
import { registerOutcomeTool } from "./register-outcome.ts";
|
|
13
13
|
import { registerToolRenderers } from "./register-renderers.ts";
|
|
14
14
|
import { ScoutStatusManager } from "./scout-status.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { sessionStateFor } from "./session.ts";
|
|
16
16
|
import type {
|
|
17
17
|
ToolRegistrationContext,
|
|
18
18
|
ToolRegistrationDependencies,
|
|
@@ -20,7 +20,7 @@ import type {
|
|
|
20
20
|
|
|
21
21
|
export const registerAdvisorTool = (
|
|
22
22
|
pi: ExtensionAPI,
|
|
23
|
-
session: AdvisorSessionState =
|
|
23
|
+
session: AdvisorSessionState = sessionStateFor(pi),
|
|
24
24
|
dependencies: ToolRegistrationDependencies = {}
|
|
25
25
|
): void => {
|
|
26
26
|
const registration: ToolRegistrationContext = {
|
|
@@ -24,12 +24,27 @@ const advisorResultDetails = (result: AgentToolResult<AdvisorToolDetails>) =>
|
|
|
24
24
|
|
|
25
25
|
const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
|
|
26
26
|
if (context.state.phase !== phase && context.state.timerId) {
|
|
27
|
-
|
|
27
|
+
clearTimeout(context.state.timerId);
|
|
28
28
|
context.state.timerId = undefined;
|
|
29
29
|
}
|
|
30
30
|
context.state.phase = phase;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
const scheduleRender = (context: AdvisorToolContext) => {
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
if (context.state.timerId !== timer) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
context.state.timerId = undefined;
|
|
39
|
+
try {
|
|
40
|
+
context.invalidate();
|
|
41
|
+
} catch {
|
|
42
|
+
// The render slot may have been discarded before the repaint fired.
|
|
43
|
+
}
|
|
44
|
+
}, 80);
|
|
45
|
+
context.state.timerId = timer;
|
|
46
|
+
};
|
|
47
|
+
|
|
33
48
|
const formatByteSize = (bytes: number): string => {
|
|
34
49
|
if (bytes >= 1024 ** 3) {
|
|
35
50
|
return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
@@ -100,15 +115,7 @@ const renderPartialAdvisorResult = (
|
|
|
100
115
|
scout?.status === "calling" || scout?.status === "streaming";
|
|
101
116
|
syncRenderPhase(context, scoutActive ? "scout" : "advisor");
|
|
102
117
|
if (!context.state.timerId) {
|
|
103
|
-
context
|
|
104
|
-
try {
|
|
105
|
-
context.invalidate();
|
|
106
|
-
} catch {
|
|
107
|
-
// Renderer context is gone; stop repainting and drop the timer.
|
|
108
|
-
clearInterval(context.state.timerId);
|
|
109
|
-
context.state.timerId = undefined;
|
|
110
|
-
}
|
|
111
|
-
}, 80);
|
|
118
|
+
scheduleRender(context);
|
|
112
119
|
}
|
|
113
120
|
if (scout) {
|
|
114
121
|
renderScoutDetails(box, scout, expanded, theme);
|
|
@@ -177,7 +184,7 @@ const renderFinalAdvisorResult = (
|
|
|
177
184
|
) => {
|
|
178
185
|
syncRenderPhase(context, "final");
|
|
179
186
|
if (context.state.timerId) {
|
|
180
|
-
|
|
187
|
+
clearTimeout(context.state.timerId);
|
|
181
188
|
context.state.timerId = undefined;
|
|
182
189
|
}
|
|
183
190
|
const details = advisorResultDetails(result);
|
package/src/tools/session.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
1
3
|
import { AdvisorSessionState } from "../session-state.ts";
|
|
2
4
|
|
|
3
5
|
export const advisorSessionState = new AdvisorSessionState();
|
|
6
|
+
|
|
7
|
+
const sessionStates = new WeakMap<object, AdvisorSessionState>();
|
|
8
|
+
|
|
9
|
+
export const sessionStateFor = (owner: ExtensionAPI): AdvisorSessionState => {
|
|
10
|
+
const existing = sessionStates.get(owner);
|
|
11
|
+
if (existing) {
|
|
12
|
+
return existing;
|
|
13
|
+
}
|
|
14
|
+
const state = new AdvisorSessionState();
|
|
15
|
+
sessionStates.set(owner, state);
|
|
16
|
+
return state;
|
|
17
|
+
};
|
package/src/ui/manual-dialog.ts
CHANGED
|
@@ -24,6 +24,7 @@ const TUI_INPUT_TAB: keyof Keybindings = ["tui", "input", "tab"].join(
|
|
|
24
24
|
interface ManualDialogState {
|
|
25
25
|
completed: boolean;
|
|
26
26
|
focused: boolean;
|
|
27
|
+
submitting: boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const isShiftTab = (keyData: string): boolean =>
|
|
@@ -40,6 +41,7 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
40
41
|
private readonly state: ManualDialogState = {
|
|
41
42
|
completed: false,
|
|
42
43
|
focused: false,
|
|
44
|
+
submitting: false,
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
get focused(): boolean {
|
|
@@ -273,17 +275,22 @@ export class ManualAdvisorDialog implements Component, Focusable {
|
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
private submit(message = this.editor.getText()): void {
|
|
276
|
-
if (this.state.completed) {
|
|
278
|
+
if (this.state.completed || this.state.submitting) {
|
|
277
279
|
return;
|
|
278
280
|
}
|
|
279
|
-
this.state.
|
|
281
|
+
this.state.submitting = true;
|
|
280
282
|
const request: ManualAdvisorRequest = {
|
|
281
283
|
gitContext: this.gitLevels[this.gitIndex] ?? "off",
|
|
282
284
|
};
|
|
283
285
|
if (message) {
|
|
284
286
|
request.message = message;
|
|
285
287
|
}
|
|
286
|
-
|
|
288
|
+
try {
|
|
289
|
+
this.options.onSubmit(request);
|
|
290
|
+
this.state.completed = true;
|
|
291
|
+
} finally {
|
|
292
|
+
this.state.submitting = false;
|
|
293
|
+
}
|
|
287
294
|
}
|
|
288
295
|
|
|
289
296
|
private cancel(): void {
|
package/src/usage.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Usage } from "@earendil-works/pi-ai/compat";
|
|
2
2
|
|
|
3
3
|
import { isNumber, isRecordOf } from "./content-utils.ts";
|
|
4
|
-
import type { JsonValue } from "./content-utils.ts";
|
|
4
|
+
import type { JsonValue, RecordValue } from "./content-utils.ts";
|
|
5
5
|
|
|
6
6
|
/** Normalized usage returned by an Advisor or Scout provider response. */
|
|
7
7
|
export interface AdvisorUsageSnapshot {
|
|
@@ -33,6 +33,18 @@ const add = (left: number | undefined, right: number | undefined) =>
|
|
|
33
33
|
|
|
34
34
|
const costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
|
|
35
35
|
|
|
36
|
+
const hasPartialProviderCost = (
|
|
37
|
+
snapshot: AdvisorUsageSnapshot,
|
|
38
|
+
usage: RecordValue
|
|
39
|
+
) => {
|
|
40
|
+
const cost = isRecordOf(usage.cost) ? usage.cost : undefined;
|
|
41
|
+
return (
|
|
42
|
+
cost !== undefined &&
|
|
43
|
+
snapshot.cost === undefined &&
|
|
44
|
+
costFields.some((field) => finite(cost[field]) !== undefined)
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
36
48
|
/** Extracts provider-agnostic usage fields without trusting provider metadata. */
|
|
37
49
|
export const snapshotAdvisorUsage = <Input>(
|
|
38
50
|
usage: Input
|
|
@@ -62,23 +74,39 @@ export const snapshotAdvisorUsage = <Input>(
|
|
|
62
74
|
export const advisorUsageCost = <Input>(usage: Input): number | undefined =>
|
|
63
75
|
snapshotAdvisorUsage(usage)?.cost;
|
|
64
76
|
|
|
77
|
+
const completeUsageSnapshot = <Input>(
|
|
78
|
+
usage: Input
|
|
79
|
+
): { snapshot: AdvisorUsageSnapshot; usage: RecordValue } | undefined => {
|
|
80
|
+
const snapshot = snapshotAdvisorUsage(usage);
|
|
81
|
+
if (!(snapshot && isRecordOf(usage))) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (hasPartialProviderCost(snapshot, usage)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
return { snapshot, usage };
|
|
88
|
+
};
|
|
89
|
+
|
|
65
90
|
/**
|
|
66
91
|
* Converts supported provider usage to Pi's complete nested-tool usage shape.
|
|
67
92
|
* Missing fields become zero only at this Pi API boundary; absent usage remains
|
|
68
93
|
* undefined so an unavailable request is never presented as a zero-cost call.
|
|
69
94
|
*/
|
|
70
95
|
export const advisorUsageForPi = <Input>(usage: Input): Usage | undefined => {
|
|
71
|
-
const
|
|
72
|
-
if (!
|
|
96
|
+
const complete = completeUsageSnapshot(usage);
|
|
97
|
+
if (!complete) {
|
|
73
98
|
return undefined;
|
|
74
99
|
}
|
|
75
|
-
const
|
|
100
|
+
const { snapshot } = complete;
|
|
101
|
+
const cost = isRecordOf(complete.usage.cost)
|
|
102
|
+
? complete.usage.cost
|
|
103
|
+
: undefined;
|
|
76
104
|
const input = snapshot.input ?? 0;
|
|
77
105
|
const output = snapshot.output ?? 0;
|
|
78
106
|
const cacheRead = snapshot.cacheRead ?? 0;
|
|
79
107
|
const cacheWrite = snapshot.cacheWrite ?? 0;
|
|
80
|
-
const cacheWrite1h = finite(usage.cacheWrite1h);
|
|
81
|
-
const reasoning = finite(usage.reasoning);
|
|
108
|
+
const cacheWrite1h = finite(complete.usage.cacheWrite1h);
|
|
109
|
+
const reasoning = finite(complete.usage.reasoning);
|
|
82
110
|
const piUsage: Usage = {
|
|
83
111
|
cacheRead,
|
|
84
112
|
cacheWrite,
|