@pfoundation/ocadvisor 26.9.1 → 26.9.3
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/README.md +444 -46
- package/dist/artificialAnalysis.d.ts +4 -0
- package/dist/artificialAnalysis.d.ts.map +1 -0
- package/dist/artificialAnalysis.js +77 -0
- package/dist/artificialAnalysis.js.map +1 -0
- package/dist/benchmarkConfig.d.ts +21 -0
- package/dist/benchmarkConfig.d.ts.map +1 -0
- package/dist/benchmarkConfig.js +59 -0
- package/dist/benchmarkConfig.js.map +1 -0
- package/dist/benchmarkEvidence.d.ts +37 -0
- package/dist/benchmarkEvidence.d.ts.map +1 -0
- package/dist/benchmarkEvidence.js +81 -0
- package/dist/benchmarkEvidence.js.map +1 -0
- package/dist/benchmarkMatch.d.ts +58 -0
- package/dist/benchmarkMatch.d.ts.map +1 -0
- package/dist/benchmarkMatch.js +259 -0
- package/dist/benchmarkMatch.js.map +1 -0
- package/dist/benchmarkStore.d.ts +45 -0
- package/dist/benchmarkStore.d.ts.map +1 -0
- package/dist/benchmarkStore.js +309 -0
- package/dist/benchmarkStore.js.map +1 -0
- package/dist/benchmarkTypes.d.ts +48 -0
- package/dist/benchmarkTypes.d.ts.map +1 -0
- package/dist/benchmarkTypes.js +298 -0
- package/dist/benchmarkTypes.js.map +1 -0
- package/dist/benchmarkUpdate.d.ts +33 -0
- package/dist/benchmarkUpdate.d.ts.map +1 -0
- package/dist/benchmarkUpdate.js +327 -0
- package/dist/benchmarkUpdate.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +397 -0
- package/dist/cli.js.map +1 -0
- package/dist/data/artificialAnalysis.mappings.json +229 -0
- package/dist/data/artificialAnalysis.snapshot.json +17684 -0
- package/dist/modelProfiles.d.ts +45 -0
- package/dist/modelProfiles.d.ts.map +1 -0
- package/dist/modelProfiles.js +141 -0
- package/dist/modelProfiles.js.map +1 -0
- package/dist/ocAdvisor.d.ts +105 -5
- package/dist/ocAdvisor.d.ts.map +1 -1
- package/dist/ocAdvisor.js +534 -45
- package/dist/ocAdvisor.js.map +1 -1
- package/dist/typesafeGate.d.ts +96 -0
- package/dist/typesafeGate.d.ts.map +1 -0
- package/dist/typesafeGate.js +475 -0
- package/dist/typesafeGate.js.map +1 -0
- package/dist/typesafeState.d.ts +94 -0
- package/dist/typesafeState.d.ts.map +1 -0
- package/dist/typesafeState.js +301 -0
- package/dist/typesafeState.js.map +1 -0
- package/package.json +8 -2
package/dist/ocAdvisor.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { appendFile } from "fs/promises";
|
|
3
3
|
import { homedir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
4
|
+
import { isAbsolute, join } from "path";
|
|
5
|
+
import { resolveBenchmarkPaths } from "./benchmarkConfig.js";
|
|
6
|
+
import { buildBenchmarkEvidence, } from "./benchmarkEvidence.js";
|
|
7
|
+
import { createBenchmarkStore, } from "./benchmarkStore.js";
|
|
8
|
+
import { resolveAdvisorProfile, resolveRequesterProfile, } from "./modelProfiles.js";
|
|
9
|
+
import { runTypeSafeGate, } from "./typesafeGate.js";
|
|
10
|
+
import { TYPESAFE_DEFAULTS, buildDecisionState, normalizeTypeSafeOptions, resolveTypeSafeConfig, } from "./typesafeState.js";
|
|
5
11
|
const DB_PATH = join(homedir(), ".local/share/opencode/opencode.db");
|
|
6
12
|
const METRICS_PATH = join(homedir(), ".local/share/opencode/ocAdvisor-metrics.jsonl");
|
|
7
13
|
const ADVISOR_PROVIDER = "anthropic";
|
|
8
14
|
const ADVISOR_MODEL = "claude-fable-5-1";
|
|
9
|
-
const ADVISOR_VARIANT = "
|
|
15
|
+
const ADVISOR_VARIANT = "xhigh";
|
|
10
16
|
const ADVISOR_SESSION_TITLE = "advisor";
|
|
11
17
|
// Sessions created before the ocAdvisor → advisor rename keep working: title
|
|
12
18
|
// discovery accepts both, and the storage key below is unchanged.
|
|
@@ -24,6 +30,11 @@ const DEFAULT_ADVISOR_CONFIG = {
|
|
|
24
30
|
timeoutMs: ADVISOR_TIMEOUT_MS,
|
|
25
31
|
maxTranscriptChars: 0,
|
|
26
32
|
agentEffort: null,
|
|
33
|
+
disabledForModels: [],
|
|
34
|
+
benchmarks: {},
|
|
35
|
+
typesafeSource: { disabled: false, overrides: {} },
|
|
36
|
+
typesafe: { enabled: false, settings: null, keyPresent: false },
|
|
37
|
+
typesafeSettings: null,
|
|
27
38
|
};
|
|
28
39
|
function normalizeVariant(value) {
|
|
29
40
|
if (value === null || value === undefined)
|
|
@@ -72,6 +83,74 @@ function normalizeAgentEffort(value) {
|
|
|
72
83
|
}
|
|
73
84
|
return undefined;
|
|
74
85
|
}
|
|
86
|
+
// Exact `provider/model` IDs. A string is split on commas; blanks are
|
|
87
|
+
// dropped. `undefined` means "not set" so a later source can keep the
|
|
88
|
+
// previous list. An empty array or whitespace-only string clears it.
|
|
89
|
+
function normalizeDisabledForModels(value) {
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
const entries = typeof value === "string" ? value.split(",") : value;
|
|
93
|
+
if (!Array.isArray(entries) ||
|
|
94
|
+
entries.some((entry) => typeof entry !== "string")) {
|
|
95
|
+
throw new Error("disabledForModels must be a string array or comma-separated string.");
|
|
96
|
+
}
|
|
97
|
+
const refs = entries.map((entry) => entry.trim()).filter(Boolean);
|
|
98
|
+
for (const ref of refs) {
|
|
99
|
+
const slash = ref.indexOf("/");
|
|
100
|
+
if (slash <= 0 ||
|
|
101
|
+
slash === ref.length - 1 ||
|
|
102
|
+
ref.endsWith("/") ||
|
|
103
|
+
ref.includes("//") ||
|
|
104
|
+
/[\s#*?]/u.test(ref)) {
|
|
105
|
+
throw new Error("disabledForModels requires exact provider/model IDs without variants or wildcards.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [...new Set(refs)];
|
|
109
|
+
}
|
|
110
|
+
// `{ path, mappingsPath, matchAnyProvider }`. `undefined`/`null` means
|
|
111
|
+
// "not set"; blank entries are dropped; unknown keys are ignored. Invalid
|
|
112
|
+
// explicit values throw so misconfiguration fails fast at setup instead of
|
|
113
|
+
// silently degrading to default benchmark data.
|
|
114
|
+
function parseBooleanOption(name, value) {
|
|
115
|
+
if (typeof value === "boolean")
|
|
116
|
+
return value;
|
|
117
|
+
if (typeof value === "string") {
|
|
118
|
+
const lowered = value.trim().toLowerCase();
|
|
119
|
+
if (["true", "1", "yes", "on"].includes(lowered))
|
|
120
|
+
return true;
|
|
121
|
+
if (["false", "0", "no", "off", ""].includes(lowered))
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`benchmarks.${name} must be true or false.`);
|
|
125
|
+
}
|
|
126
|
+
function normalizeBenchmarks(value) {
|
|
127
|
+
if (value === undefined || value === null)
|
|
128
|
+
return undefined;
|
|
129
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
130
|
+
throw new Error("benchmarks must be an object with path options.");
|
|
131
|
+
}
|
|
132
|
+
const raw = value;
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const key of ["path", "mappingsPath"]) {
|
|
135
|
+
const entry = raw[key];
|
|
136
|
+
if (entry === undefined || entry === null)
|
|
137
|
+
continue;
|
|
138
|
+
if (typeof entry !== "string") {
|
|
139
|
+
throw new Error(`benchmarks.${key} must be an absolute path.`);
|
|
140
|
+
}
|
|
141
|
+
const trimmed = entry.trim();
|
|
142
|
+
if (!trimmed)
|
|
143
|
+
continue;
|
|
144
|
+
if (!isAbsolute(trimmed)) {
|
|
145
|
+
throw new Error(`benchmarks.${key} must be an absolute path.`);
|
|
146
|
+
}
|
|
147
|
+
out[key] = trimmed;
|
|
148
|
+
}
|
|
149
|
+
if (raw.matchAnyProvider !== undefined && raw.matchAnyProvider !== null) {
|
|
150
|
+
out.matchAnyProvider = parseBooleanOption("matchAnyProvider", raw.matchAnyProvider);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
75
154
|
function toBoundedInt(value, min) {
|
|
76
155
|
const raw = typeof value === "number"
|
|
77
156
|
? value
|
|
@@ -108,7 +187,7 @@ function parseModelRef(ref) {
|
|
|
108
187
|
}
|
|
109
188
|
return out;
|
|
110
189
|
}
|
|
111
|
-
function applyAdvisorConfigSource(base, src) {
|
|
190
|
+
function applyAdvisorConfigSource(base, src, env = process.env) {
|
|
112
191
|
if (!src || typeof src !== "object")
|
|
113
192
|
return base;
|
|
114
193
|
const next = { ...base };
|
|
@@ -135,12 +214,31 @@ function applyAdvisorConfigSource(base, src) {
|
|
|
135
214
|
if (normalized !== undefined)
|
|
136
215
|
next.agentEffort = normalized;
|
|
137
216
|
}
|
|
217
|
+
const disabledForModels = normalizeDisabledForModels(src.disabledForModels);
|
|
218
|
+
if (disabledForModels !== undefined) {
|
|
219
|
+
next.disabledForModels = disabledForModels;
|
|
220
|
+
}
|
|
221
|
+
// Benchmark paths merge per field so a plugin option can override one
|
|
222
|
+
// location while the other still falls back to the environment default.
|
|
223
|
+
const benchmarks = normalizeBenchmarks(src.benchmarks);
|
|
224
|
+
if (benchmarks !== undefined) {
|
|
225
|
+
next.benchmarks = { ...next.benchmarks, ...benchmarks };
|
|
226
|
+
}
|
|
138
227
|
const timeout = toBoundedInt(src.timeoutMs ?? src.timeout_ms, 1);
|
|
139
228
|
if (timeout !== undefined)
|
|
140
229
|
next.timeoutMs = timeout;
|
|
141
230
|
const cap = toBoundedInt(src.maxTranscriptChars ?? src.max_transcript_chars, 0);
|
|
142
231
|
if (cap !== undefined)
|
|
143
232
|
next.maxTranscriptChars = cap;
|
|
233
|
+
if (src.typesafe !== undefined) {
|
|
234
|
+
const normalized = normalizeTypeSafeOptions(src.typesafe);
|
|
235
|
+
if ("error" in normalized) {
|
|
236
|
+
throw new Error(normalized.error);
|
|
237
|
+
}
|
|
238
|
+
next.typesafeSource = normalized;
|
|
239
|
+
}
|
|
240
|
+
next.typesafe = resolveTypeSafeConfig(next.typesafeSource, env);
|
|
241
|
+
next.typesafeSettings = next.typesafe.settings;
|
|
144
242
|
return next;
|
|
145
243
|
}
|
|
146
244
|
function envAdvisorConfigSource(env = process.env) {
|
|
@@ -151,14 +249,36 @@ function envAdvisorConfigSource(env = process.env) {
|
|
|
151
249
|
timeoutMs: env.OCADVISOR_TIMEOUT_MS,
|
|
152
250
|
maxTranscriptChars: env.OCADVISOR_MAX_TRANSCRIPT_CHARS,
|
|
153
251
|
agentEffort: env.OCADVISOR_AGENT_EFFORT,
|
|
252
|
+
disabledForModels: env.OCADVISOR_DISABLED_FOR_MODELS,
|
|
253
|
+
benchmarks: env.OCADVISOR_BENCHMARKS_PATH === undefined &&
|
|
254
|
+
env.OCADVISOR_BENCHMARK_MAPPINGS_PATH === undefined &&
|
|
255
|
+
env.OCADVISOR_BENCHMARKS_MATCH_ANY_PROVIDER === undefined
|
|
256
|
+
? undefined
|
|
257
|
+
: {
|
|
258
|
+
path: env.OCADVISOR_BENCHMARKS_PATH,
|
|
259
|
+
mappingsPath: env.OCADVISOR_BENCHMARK_MAPPINGS_PATH,
|
|
260
|
+
...(env.OCADVISOR_BENCHMARKS_MATCH_ANY_PROVIDER !== undefined
|
|
261
|
+
? {
|
|
262
|
+
matchAnyProvider: env.OCADVISOR_BENCHMARKS_MATCH_ANY_PROVIDER,
|
|
263
|
+
}
|
|
264
|
+
: {}),
|
|
265
|
+
},
|
|
154
266
|
};
|
|
155
267
|
}
|
|
156
268
|
// Precedence (low to high): built-in defaults, environment variables,
|
|
157
269
|
// plugin options from opencode.json.
|
|
158
270
|
function resolveAdvisorConfig(options, env = process.env) {
|
|
159
|
-
let config = applyAdvisorConfigSource(DEFAULT_ADVISOR_CONFIG, envAdvisorConfigSource(env));
|
|
160
|
-
config = applyAdvisorConfigSource(config, options);
|
|
161
|
-
|
|
271
|
+
let config = applyAdvisorConfigSource(DEFAULT_ADVISOR_CONFIG, envAdvisorConfigSource(env), env);
|
|
272
|
+
config = applyAdvisorConfigSource(config, options, env);
|
|
273
|
+
// Resolved view always states the matching policy, so downstream code
|
|
274
|
+
// never has to invent the documented default.
|
|
275
|
+
return {
|
|
276
|
+
...config,
|
|
277
|
+
benchmarks: {
|
|
278
|
+
...config.benchmarks,
|
|
279
|
+
matchAnyProvider: config.benchmarks.matchAnyProvider ?? false,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
162
282
|
}
|
|
163
283
|
const SYSTEM_BASE = `You are a senior advisor reviewing a coding agent's work. You have the full session transcript.
|
|
164
284
|
Respond in 500-750 words with structured analysis and enumerated steps. Be direct and actionable.`;
|
|
@@ -174,15 +294,17 @@ You are a debugger. Analyze error patterns, stack traces, and failed attempts in
|
|
|
174
294
|
};
|
|
175
295
|
const TOOL_DESCRIPTION = `Consult a senior advisor model with your full session transcript — including parent sessions for subagents — for high-quality analysis.
|
|
176
296
|
|
|
177
|
-
Use advisor
|
|
297
|
+
Use advisor when an independent perspective could improve the approach, help resolve a problem, or strengthen an implementation review.
|
|
298
|
+
|
|
299
|
+
On substantial work, consider consulting before committing to an approach, when progress stalls, or before completing meaningful changes. Additional consultations are welcome as the work evolves — particularly when new evidence appears, the approach changes, or another concern needs review.
|
|
178
300
|
|
|
179
|
-
|
|
180
|
-
- mode "general": a second opinion that does not fit the above.
|
|
301
|
+
Ask a concrete, focused question. Avoid repeating settled questions without new context. Straightforward tasks usually need no consultation.
|
|
181
302
|
|
|
182
303
|
Rules:
|
|
183
304
|
- Always pass a concrete "question" naming the decision or artifact under review.
|
|
184
|
-
-
|
|
305
|
+
- Reconcile an advisor conflict with primary-source evidence via one "followup" call stating both sides.
|
|
185
306
|
- Give the advice serious weight. A passing self-test alone is not counter-evidence; primary-source evidence (the file says X) is. Clear factual corrections do not need another confirmation call.
|
|
307
|
+
- When TypeSafe screening is enabled, a clearly unnecessary consultation returns a skip notice instead of advice, and an omitted effort may be chosen for you.
|
|
186
308
|
|
|
187
309
|
Args: "mode" (general, review, plan, debug), "trigger" (before_approach, stuck, pre_complete, followup, other), "question" (concrete question focusing the advisor).
|
|
188
310
|
`;
|
|
@@ -195,7 +317,7 @@ function buildToolDescription(config = DEFAULT_ADVISOR_CONFIG) {
|
|
|
195
317
|
return (TOOL_DESCRIPTION +
|
|
196
318
|
`Optional "effort" (one of: ${config.agentEffort.join(", ")}): reasoning effort for this consultation; omit to use the configured variant.\n`);
|
|
197
319
|
}
|
|
198
|
-
const CHECKPOINT_INSTRUCTION = `[advisor]
|
|
320
|
+
const CHECKPOINT_INSTRUCTION = `[advisor] Consult advisor when an independent perspective would improve the approach, help resolve a problem, or strengthen review. Use useful checkpoints during substantial work; additional consultations are welcome when evidence, approach, or concerns change. Ask a concrete question and avoid repeating settled questions without new context.`;
|
|
199
321
|
const ADVISOR_TRIGGERS = [
|
|
200
322
|
"before_approach",
|
|
201
323
|
"stuck",
|
|
@@ -240,8 +362,10 @@ function buildAdvisorInputSchema(config = DEFAULT_ADVISOR_CONFIG) {
|
|
|
240
362
|
},
|
|
241
363
|
};
|
|
242
364
|
}
|
|
243
|
-
function openDb() {
|
|
244
|
-
return new Database(DB_PATH, {
|
|
365
|
+
function openDb(runtime) {
|
|
366
|
+
return new Database(runtime?.__advisorTest?.dbPath ?? DB_PATH, {
|
|
367
|
+
readonly: true,
|
|
368
|
+
});
|
|
245
369
|
}
|
|
246
370
|
function tableExists(db, name) {
|
|
247
371
|
const row = db
|
|
@@ -267,6 +391,22 @@ function isFableModel(model) {
|
|
|
267
391
|
const id = String(model.id || model.modelID || "").toLowerCase();
|
|
268
392
|
return provider.includes("anthropic") && id.includes("fable");
|
|
269
393
|
}
|
|
394
|
+
function advisorDisabledReason(model, config) {
|
|
395
|
+
if (isFableModel(model)) {
|
|
396
|
+
return { outcome: "skipped_fable", message: FABLE_DISABLED };
|
|
397
|
+
}
|
|
398
|
+
const provider = model?.providerID || model?.provider;
|
|
399
|
+
const id = model?.id || model?.modelID;
|
|
400
|
+
if (!provider || !id)
|
|
401
|
+
return null;
|
|
402
|
+
const ref = `${provider}/${id}`;
|
|
403
|
+
if (!config.disabledForModels.includes(ref))
|
|
404
|
+
return null;
|
|
405
|
+
return {
|
|
406
|
+
outcome: "skipped_model",
|
|
407
|
+
message: `advisor is disabled (model opt-out): ${ref} is listed in disabledForModels.`,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
270
410
|
function inferTrigger(mode, trigger) {
|
|
271
411
|
if (trigger && ADVISOR_TRIGGERS.includes(trigger)) {
|
|
272
412
|
return trigger;
|
|
@@ -405,6 +545,13 @@ function isAdvisorToolName(name) {
|
|
|
405
545
|
function countPriorAdvisorCalls(db, sessionId) {
|
|
406
546
|
const callIds = new Set();
|
|
407
547
|
const modes = [];
|
|
548
|
+
const questions = [];
|
|
549
|
+
const record = (input) => {
|
|
550
|
+
modes.push(String(input?.mode || "general"));
|
|
551
|
+
if (typeof input?.question === "string" && input.question.trim()) {
|
|
552
|
+
questions.push(input.question);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
408
555
|
try {
|
|
409
556
|
for (const sid of collectSessionChain(db, sessionId)) {
|
|
410
557
|
if (tableExists(db, "session_message")) {
|
|
@@ -433,7 +580,7 @@ function countPriorAdvisorCalls(db, sessionId) {
|
|
|
433
580
|
const cid = String(block.id || block.callID || row.id);
|
|
434
581
|
if (!callIds.has(cid)) {
|
|
435
582
|
callIds.add(cid);
|
|
436
|
-
|
|
583
|
+
record(state.input);
|
|
437
584
|
}
|
|
438
585
|
}
|
|
439
586
|
nested.forEach((call, index) => {
|
|
@@ -441,7 +588,7 @@ function countPriorAdvisorCalls(db, sessionId) {
|
|
|
441
588
|
const cid = `${block.id || row.id}#${index}`;
|
|
442
589
|
if (!callIds.has(cid)) {
|
|
443
590
|
callIds.add(cid);
|
|
444
|
-
|
|
591
|
+
record(call.input);
|
|
445
592
|
}
|
|
446
593
|
}
|
|
447
594
|
});
|
|
@@ -469,22 +616,88 @@ function countPriorAdvisorCalls(db, sessionId) {
|
|
|
469
616
|
if (!cid || callIds.has(cid))
|
|
470
617
|
continue;
|
|
471
618
|
callIds.add(cid);
|
|
472
|
-
|
|
619
|
+
record(block.state?.input);
|
|
473
620
|
}
|
|
474
621
|
}
|
|
475
622
|
}
|
|
476
623
|
}
|
|
477
624
|
catch { }
|
|
478
|
-
return { count: callIds.size, modes };
|
|
625
|
+
return { count: callIds.size, modes, questions };
|
|
479
626
|
}
|
|
480
|
-
async function logAdvisorMetrics(metrics) {
|
|
627
|
+
async function logAdvisorMetrics(runtime, metrics) {
|
|
481
628
|
try {
|
|
482
|
-
|
|
629
|
+
const path = runtime?.__advisorTest?.metricsPath ?? METRICS_PATH;
|
|
630
|
+
await appendFile(path, JSON.stringify(metrics) + "\n", "utf-8");
|
|
483
631
|
}
|
|
484
632
|
catch {
|
|
485
633
|
// Metrics must never break the advisor call.
|
|
486
634
|
}
|
|
487
635
|
}
|
|
636
|
+
// Flattens a gate decision into the additive metrics record. A bypassed gate
|
|
637
|
+
// reports `bypass` so reports can separate bypass, skip, fallback, and
|
|
638
|
+
// allowed proceed without inferring from other fields.
|
|
639
|
+
function gateRecordFrom(decision) {
|
|
640
|
+
if (decision.status === "disabled") {
|
|
641
|
+
return {
|
|
642
|
+
status: "bypass",
|
|
643
|
+
reason: "disabled",
|
|
644
|
+
model: null,
|
|
645
|
+
neededProbability: null,
|
|
646
|
+
suggestedEffort: null,
|
|
647
|
+
effortConfidence: null,
|
|
648
|
+
effectiveEffort: null,
|
|
649
|
+
effortSource: null,
|
|
650
|
+
latencyMs: 0,
|
|
651
|
+
stateBytes: 0,
|
|
652
|
+
truncated: false,
|
|
653
|
+
inputTokens: null,
|
|
654
|
+
outputTokens: null,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
const metrics = decision.metrics;
|
|
658
|
+
return {
|
|
659
|
+
status: decision.status,
|
|
660
|
+
reason: decision.reason,
|
|
661
|
+
model: metrics.model,
|
|
662
|
+
neededProbability: metrics.neededProbability,
|
|
663
|
+
suggestedEffort: metrics.suggestedEffort,
|
|
664
|
+
effortConfidence: metrics.effortConfidence,
|
|
665
|
+
effectiveEffort: decision.status === "proceed" ? decision.effectiveEffort : null,
|
|
666
|
+
effortSource: decision.status === "proceed" ? decision.effortSource : null,
|
|
667
|
+
latencyMs: metrics.latencyMs,
|
|
668
|
+
stateBytes: metrics.stateBytes,
|
|
669
|
+
truncated: metrics.truncated,
|
|
670
|
+
inputTokens: metrics.inputTokens,
|
|
671
|
+
outputTokens: metrics.outputTokens,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
// Human-readable gate summary for the tool output. A bypassed gate contributes
|
|
675
|
+
// nothing, so keyless or disabled installs keep the plain footer.
|
|
676
|
+
export function gateSummary(gate) {
|
|
677
|
+
if (!gate || gate.status === "bypass")
|
|
678
|
+
return null;
|
|
679
|
+
const parts = [];
|
|
680
|
+
if (gate.neededProbability !== null) {
|
|
681
|
+
parts.push(`need=${gate.neededProbability.toFixed(2)}`);
|
|
682
|
+
}
|
|
683
|
+
if (gate.effectiveEffort) {
|
|
684
|
+
parts.push(gate.effortSource === "typesafe"
|
|
685
|
+
? `effort=${gate.effectiveEffort} (gate)`
|
|
686
|
+
: `effort=${gate.effectiveEffort}`);
|
|
687
|
+
}
|
|
688
|
+
switch (gate.status) {
|
|
689
|
+
case "skip":
|
|
690
|
+
parts.push("decision=skip", gate.reason);
|
|
691
|
+
break;
|
|
692
|
+
case "fallback":
|
|
693
|
+
parts.push("decision=fallback", gate.reason);
|
|
694
|
+
break;
|
|
695
|
+
default:
|
|
696
|
+
parts.push("decision=proceed");
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
return `gate: ${parts.join(", ")}`;
|
|
700
|
+
}
|
|
488
701
|
function getSession(db, sessionId) {
|
|
489
702
|
return db
|
|
490
703
|
.query("SELECT id, parent_id, title, directory FROM session WHERE id = ?")
|
|
@@ -759,6 +972,20 @@ function resolveAdvisorVariant(model, config = DEFAULT_ADVISOR_CONFIG, requested
|
|
|
759
972
|
const ids = model.variants.map((variant) => variant?.id);
|
|
760
973
|
return ids.includes(config.variant) ? config.variant : undefined;
|
|
761
974
|
}
|
|
975
|
+
// Effort levels the gate may choose: plugin-allowed candidates intersected
|
|
976
|
+
// with the model's live variants. Unknown discovery yields no candidates,
|
|
977
|
+
// which keeps the configured effort and omits automatic effort selection.
|
|
978
|
+
function resolveSupportedEfforts(model, config) {
|
|
979
|
+
const allowed = config.agentEffort && config.agentEffort.length > 0
|
|
980
|
+
? config.agentEffort
|
|
981
|
+
: null;
|
|
982
|
+
if (!allowed || !model || !Array.isArray(model.variants))
|
|
983
|
+
return [];
|
|
984
|
+
const variants = new Set(model.variants
|
|
985
|
+
.map((variant) => variant?.id)
|
|
986
|
+
.filter((id) => !!id));
|
|
987
|
+
return allowed.filter((effort) => variants.has(effort));
|
|
988
|
+
}
|
|
762
989
|
function hasAdvisorConnection(connection) {
|
|
763
990
|
if (connection === null || connection === undefined)
|
|
764
991
|
return false;
|
|
@@ -772,11 +999,50 @@ function hasAdvisorConnection(connection) {
|
|
|
772
999
|
}
|
|
773
1000
|
return true;
|
|
774
1001
|
}
|
|
1002
|
+
// Discovery namespaces: the documented V2 `model`/`provider` domains win,
|
|
1003
|
+
// the legacy `catalog` namespace stays for older servers and fixtures.
|
|
1004
|
+
// Calls stay in method form so any receiver-bound implementation keeps
|
|
1005
|
+
// working.
|
|
1006
|
+
function discoveryModelApi(runtime) {
|
|
1007
|
+
if (runtime.model && typeof runtime.model.list === "function") {
|
|
1008
|
+
return runtime.model;
|
|
1009
|
+
}
|
|
1010
|
+
if (runtime.catalog?.model &&
|
|
1011
|
+
typeof runtime.catalog.model.list === "function") {
|
|
1012
|
+
return runtime.catalog.model;
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
function discoveryProviderApi(runtime) {
|
|
1017
|
+
if (runtime.provider && typeof runtime.provider.get === "function") {
|
|
1018
|
+
return runtime.provider;
|
|
1019
|
+
}
|
|
1020
|
+
if (runtime.catalog?.provider &&
|
|
1021
|
+
typeof runtime.catalog.provider.get === "function") {
|
|
1022
|
+
return runtime.catalog.provider;
|
|
1023
|
+
}
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
// Shared by support checks and the effort gate; null means discovery is
|
|
1027
|
+
// unavailable or the configured model was not found.
|
|
1028
|
+
async function findAdvisorCatalogModel(runtime, config) {
|
|
1029
|
+
const api = discoveryModelApi(runtime);
|
|
1030
|
+
if (!api)
|
|
1031
|
+
return null;
|
|
1032
|
+
try {
|
|
1033
|
+
const models = unwrapData((await api.list()));
|
|
1034
|
+
return findAdvisorModel(models, config);
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
775
1040
|
async function checkAdvisorSupport(runtime, config = DEFAULT_ADVISOR_CONFIG, requestedVariant) {
|
|
776
|
-
|
|
1041
|
+
const providerApi = discoveryProviderApi(runtime);
|
|
1042
|
+
if (providerApi) {
|
|
777
1043
|
let provider = null;
|
|
778
1044
|
try {
|
|
779
|
-
provider = unwrapData((await
|
|
1045
|
+
provider = unwrapData((await providerApi.get({
|
|
780
1046
|
providerID: config.provider,
|
|
781
1047
|
})));
|
|
782
1048
|
}
|
|
@@ -794,10 +1060,11 @@ async function checkAdvisorSupport(runtime, config = DEFAULT_ADVISOR_CONFIG, req
|
|
|
794
1060
|
}
|
|
795
1061
|
}
|
|
796
1062
|
let variant = requestedVariant ?? config.variant;
|
|
797
|
-
|
|
1063
|
+
const modelApi = discoveryModelApi(runtime);
|
|
1064
|
+
if (modelApi) {
|
|
798
1065
|
let models = null;
|
|
799
1066
|
try {
|
|
800
|
-
models = unwrapData((await
|
|
1067
|
+
models = unwrapData((await modelApi.list()));
|
|
801
1068
|
}
|
|
802
1069
|
catch (err) {
|
|
803
1070
|
return {
|
|
@@ -1025,10 +1292,25 @@ async function callAdvisor(opts) {
|
|
|
1025
1292
|
const text = await enqueueAdvisor(() => withTimeout((async () => {
|
|
1026
1293
|
const sessionId = await ensureAdvisorSession(runtime, support.variant, config);
|
|
1027
1294
|
const request = { sessionID: sessionId, prompt };
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1295
|
+
const startedAt = Date.now();
|
|
1296
|
+
const run = async () => {
|
|
1297
|
+
const result = opts.signal
|
|
1298
|
+
? await generate(request, { signal: opts.signal })
|
|
1299
|
+
: await generate(request);
|
|
1300
|
+
return extractGeneratedText(result);
|
|
1301
|
+
};
|
|
1302
|
+
// A generation can come back without text (transient provider
|
|
1303
|
+
// behavior, e.g. a reasoning-only response with adaptive thinking).
|
|
1304
|
+
// Retry once while most of the timeout budget remains; never retry
|
|
1305
|
+
// after a caller cancellation.
|
|
1306
|
+
let output = await run();
|
|
1307
|
+
if (!output?.trim() && !opts.signal?.aborted) {
|
|
1308
|
+
const elapsed = Date.now() - startedAt;
|
|
1309
|
+
if (elapsed < config.timeoutMs / 2) {
|
|
1310
|
+
console.log(`[advisor] empty generation; retrying once (session=${sessionId} elapsedMs=${elapsed})`);
|
|
1311
|
+
output = await run();
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1032
1314
|
if (!output?.trim()) {
|
|
1033
1315
|
throw new Error("Advisor returned an empty response.");
|
|
1034
1316
|
}
|
|
@@ -1044,6 +1326,87 @@ async function callAdvisor(opts) {
|
|
|
1044
1326
|
variant: support.variant,
|
|
1045
1327
|
};
|
|
1046
1328
|
}
|
|
1329
|
+
// Benchmark stores live for the process, keyed by resolved user paths (and
|
|
1330
|
+
// test seed overrides), so every consultation observes CLI refreshes
|
|
1331
|
+
// without re-reading unchanged files. Tests reset between cases.
|
|
1332
|
+
const benchmarkStores = new Map();
|
|
1333
|
+
function resetBenchmarkStores() {
|
|
1334
|
+
benchmarkStores.clear();
|
|
1335
|
+
}
|
|
1336
|
+
function benchmarkStoreKey(snapshotPath, mappingsPath, matchAnyProvider, seedSnapshotPath, seedMappingsPath) {
|
|
1337
|
+
return [
|
|
1338
|
+
snapshotPath,
|
|
1339
|
+
mappingsPath,
|
|
1340
|
+
matchAnyProvider ? "any-provider" : "strict-provider",
|
|
1341
|
+
seedSnapshotPath ?? "",
|
|
1342
|
+
seedMappingsPath ?? "",
|
|
1343
|
+
].join("\n");
|
|
1344
|
+
}
|
|
1345
|
+
// Loads one consistent benchmark view and assembles gate evidence from it.
|
|
1346
|
+
// Any failure (bad paths, unreadable files, unexpected errors) yields null
|
|
1347
|
+
// and the consultation proceeds without benchmark enrichment.
|
|
1348
|
+
async function loadBenchmarkEvidence(runtime, config, requester, advisor) {
|
|
1349
|
+
try {
|
|
1350
|
+
const testOpts = runtime?.__advisorTest?.benchmarkStoreOptions;
|
|
1351
|
+
const paths = resolveBenchmarkPaths(config.benchmarks, testOpts?.env ?? process.env);
|
|
1352
|
+
if ("error" in paths)
|
|
1353
|
+
return null;
|
|
1354
|
+
const key = benchmarkStoreKey(paths.snapshotPath, paths.mappingsPath, config.benchmarks.matchAnyProvider ?? false, testOpts?.seedSnapshotPath, testOpts?.seedMappingsPath);
|
|
1355
|
+
let store = benchmarkStores.get(key);
|
|
1356
|
+
if (!store) {
|
|
1357
|
+
store = await createBenchmarkStore({
|
|
1358
|
+
snapshotPath: paths.snapshotPath,
|
|
1359
|
+
mappingsPath: paths.mappingsPath,
|
|
1360
|
+
seedSnapshotPath: testOpts?.seedSnapshotPath,
|
|
1361
|
+
seedMappingsPath: testOpts?.seedMappingsPath,
|
|
1362
|
+
fs: testOpts?.fs,
|
|
1363
|
+
maxBytes: testOpts?.maxBytes,
|
|
1364
|
+
matchAnyProvider: config.benchmarks.matchAnyProvider ?? false,
|
|
1365
|
+
});
|
|
1366
|
+
benchmarkStores.set(key, store);
|
|
1367
|
+
}
|
|
1368
|
+
const view = await store.view();
|
|
1369
|
+
return {
|
|
1370
|
+
view,
|
|
1371
|
+
evidence: buildBenchmarkEvidence({ requester, advisor, view }),
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
catch {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function summarizeBenchmarkEvidence(loaded, finalEffort) {
|
|
1379
|
+
if (!loaded)
|
|
1380
|
+
return undefined;
|
|
1381
|
+
const { view, evidence } = loaded;
|
|
1382
|
+
const { requester, advisor } = evidence.models;
|
|
1383
|
+
const policy = advisor.policy;
|
|
1384
|
+
const finalMatch = finalEffort === null
|
|
1385
|
+
? null
|
|
1386
|
+
: view.matcher.match({
|
|
1387
|
+
providerID: advisor.providerID,
|
|
1388
|
+
modelID: advisor.modelID,
|
|
1389
|
+
variant: finalEffort,
|
|
1390
|
+
}).status;
|
|
1391
|
+
return {
|
|
1392
|
+
source: evidence.benchmarks.source,
|
|
1393
|
+
contentHash: evidence.benchmarks.contentHash,
|
|
1394
|
+
fetchedAt: evidence.benchmarks.fetchedAt,
|
|
1395
|
+
hashVerified: evidence.benchmarks.hashVerified,
|
|
1396
|
+
requester: requester.providerID && requester.modelID
|
|
1397
|
+
? `${requester.providerID}/${requester.modelID}${requester.variant ? `#${requester.variant}` : ""} (${requester.provenance})`
|
|
1398
|
+
: null,
|
|
1399
|
+
requesterMatch: evidence.benchmarks.requesterMatch.status,
|
|
1400
|
+
advisorPolicy: policy.kind === "pinned"
|
|
1401
|
+
? `pinned:${policy.effort}`
|
|
1402
|
+
: policy.kind === "candidates"
|
|
1403
|
+
? `candidates:${policy.candidates.join(",")}>${policy.fallback ?? "none"}`
|
|
1404
|
+
: `fixed:${policy.effort ?? "none"}`,
|
|
1405
|
+
advisorMatch: evidence.benchmarks.advisorDefaultMatch.status,
|
|
1406
|
+
finalEffort,
|
|
1407
|
+
finalMatch,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1047
1410
|
async function runAdvisor(opts) {
|
|
1048
1411
|
const started = Date.now();
|
|
1049
1412
|
const config = opts.config ?? DEFAULT_ADVISOR_CONFIG;
|
|
@@ -1052,7 +1415,7 @@ async function runAdvisor(opts) {
|
|
|
1052
1415
|
const sessionId = opts.sessionId;
|
|
1053
1416
|
const questionChars = opts.question?.length ?? 0;
|
|
1054
1417
|
if (!sessionId) {
|
|
1055
|
-
await logAdvisorMetrics({
|
|
1418
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1056
1419
|
ts: new Date().toISOString(),
|
|
1057
1420
|
sessionId: null,
|
|
1058
1421
|
callerModel: null,
|
|
@@ -1075,13 +1438,14 @@ async function runAdvisor(opts) {
|
|
|
1075
1438
|
}
|
|
1076
1439
|
let db = null;
|
|
1077
1440
|
try {
|
|
1078
|
-
db = openDb();
|
|
1441
|
+
db = openDb(opts.runtime);
|
|
1079
1442
|
const info = getSessionInfo(db, sessionId);
|
|
1080
1443
|
const callerModel = callerLabel(info?.model ?? null);
|
|
1081
1444
|
const callerAgent = opts.callerAgent || info?.agent || null;
|
|
1082
1445
|
const directory = opts.callerDirectory || info?.directory || null;
|
|
1083
|
-
|
|
1084
|
-
|
|
1446
|
+
const disabled = advisorDisabledReason(info?.model, config);
|
|
1447
|
+
if (disabled) {
|
|
1448
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1085
1449
|
ts: new Date().toISOString(),
|
|
1086
1450
|
sessionId,
|
|
1087
1451
|
callerModel,
|
|
@@ -1091,7 +1455,7 @@ async function runAdvisor(opts) {
|
|
|
1091
1455
|
trigger,
|
|
1092
1456
|
questionChars,
|
|
1093
1457
|
effort: null,
|
|
1094
|
-
outcome:
|
|
1458
|
+
outcome: disabled.outcome,
|
|
1095
1459
|
errorType: null,
|
|
1096
1460
|
latencyMs: Date.now() - started,
|
|
1097
1461
|
inputTokens: null,
|
|
@@ -1100,12 +1464,12 @@ async function runAdvisor(opts) {
|
|
|
1100
1464
|
priorConsultations: 0,
|
|
1101
1465
|
via: "opencode-session",
|
|
1102
1466
|
});
|
|
1103
|
-
console.log(`[advisor] session=${sessionId} mode=${mode} outcome
|
|
1104
|
-
return
|
|
1467
|
+
console.log(`[advisor] session=${sessionId} mode=${mode} outcome=${disabled.outcome}`);
|
|
1468
|
+
return disabled.message;
|
|
1105
1469
|
}
|
|
1106
1470
|
let transcript = buildTranscript(db, sessionId);
|
|
1107
1471
|
if (!transcript?.trim()) {
|
|
1108
|
-
await logAdvisorMetrics({
|
|
1472
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1109
1473
|
ts: new Date().toISOString(),
|
|
1110
1474
|
sessionId,
|
|
1111
1475
|
callerModel,
|
|
@@ -1134,26 +1498,141 @@ async function runAdvisor(opts) {
|
|
|
1134
1498
|
"... (older transcript trimmed to fit maxTranscriptChars) ...\n\n" +
|
|
1135
1499
|
transcript.slice(-config.maxTranscriptChars);
|
|
1136
1500
|
}
|
|
1501
|
+
// Invocation-correct requester identity for benchmark matching: the
|
|
1502
|
+
// model that produced this tool call, resolved after the early guards
|
|
1503
|
+
// so skipped calls pay for no extra reads.
|
|
1504
|
+
const requesterProfile = resolveRequesterProfile(db, {
|
|
1505
|
+
sessionId,
|
|
1506
|
+
messageID: opts.callerMessageID,
|
|
1507
|
+
callID: opts.callerCallID,
|
|
1508
|
+
sessionModel: info?.model ?? null,
|
|
1509
|
+
});
|
|
1137
1510
|
const prior = countPriorAdvisorCalls(db, sessionId);
|
|
1138
1511
|
const priorNote = prior.count > 0
|
|
1139
1512
|
? `Note: this session chain already has ${prior.count} recorded advisor consultation(s) (modes: ${prior.modes.join(", ") || "unknown"}). Focus on what is new since then; do not repeat settled advice unless new evidence changes it.`
|
|
1140
1513
|
: null;
|
|
1141
1514
|
const systemPrompt = SYSTEM_PROMPTS[mode] || SYSTEM_PROMPTS.general;
|
|
1142
1515
|
let requestedEffort;
|
|
1516
|
+
let effectiveEffort;
|
|
1517
|
+
let gateRecord;
|
|
1518
|
+
let benchmarkLoaded = null;
|
|
1143
1519
|
try {
|
|
1144
1520
|
requestedEffort = resolveRequestedEffort(config, opts.effort);
|
|
1521
|
+
// Optional TypeSafe preflight: one bounded request decides whether this
|
|
1522
|
+
// consultation is worth the expensive generation and, when the caller
|
|
1523
|
+
// did not pin an effort, which supported effort fits. Gate failures
|
|
1524
|
+
// fall back to the ordinary behavior and never fail the call.
|
|
1525
|
+
let gateDecision = null;
|
|
1526
|
+
let advisorProfile = null;
|
|
1527
|
+
if (config.typesafe.keyPresent && !config.typesafeSource.disabled) {
|
|
1528
|
+
const catalogModel = await findAdvisorCatalogModel(opts.runtime, config);
|
|
1529
|
+
const supportedEfforts = resolveSupportedEfforts(catalogModel, config);
|
|
1530
|
+
advisorProfile = resolveAdvisorProfile({
|
|
1531
|
+
providerID: config.provider,
|
|
1532
|
+
modelID: config.model,
|
|
1533
|
+
requestedEffort,
|
|
1534
|
+
supportedEfforts,
|
|
1535
|
+
defaultEffort: requestedEffort ??
|
|
1536
|
+
resolveAdvisorVariant(catalogModel, config) ??
|
|
1537
|
+
null,
|
|
1538
|
+
});
|
|
1539
|
+
const gateSettings = config.typesafe.settings ?? TYPESAFE_DEFAULTS;
|
|
1540
|
+
benchmarkLoaded = await loadBenchmarkEvidence(opts.runtime, config, requesterProfile, advisorProfile);
|
|
1541
|
+
gateDecision = await runTypeSafeGate({
|
|
1542
|
+
state: buildDecisionState(db, sessionId, {
|
|
1543
|
+
question: opts.question ?? null,
|
|
1544
|
+
mode,
|
|
1545
|
+
trigger,
|
|
1546
|
+
explicitEffort: requestedEffort ?? null,
|
|
1547
|
+
caller: callerModel,
|
|
1548
|
+
directory,
|
|
1549
|
+
priorConsultations: prior.count,
|
|
1550
|
+
priorModes: prior.modes,
|
|
1551
|
+
priorQuestions: prior.questions,
|
|
1552
|
+
supportedEfforts,
|
|
1553
|
+
defaultEffort: requestedEffort ?? config.variant ?? null,
|
|
1554
|
+
maxStateBytes: gateSettings.maxStateBytes,
|
|
1555
|
+
models: benchmarkLoaded?.evidence.models ?? null,
|
|
1556
|
+
benchmarks: benchmarkLoaded?.evidence.benchmarks ?? null,
|
|
1557
|
+
formatMessage: formatV2Message,
|
|
1558
|
+
}),
|
|
1559
|
+
input: {
|
|
1560
|
+
mode,
|
|
1561
|
+
trigger,
|
|
1562
|
+
question: opts.question ?? null,
|
|
1563
|
+
explicitEffort: requestedEffort ?? null,
|
|
1564
|
+
supportedEfforts,
|
|
1565
|
+
defaultEffort: requestedEffort ?? config.variant ?? null,
|
|
1566
|
+
},
|
|
1567
|
+
settings: gateSettings,
|
|
1568
|
+
keyPresent: config.typesafe.keyPresent,
|
|
1569
|
+
client: opts.runtime?.__advisorTest?.gateClient,
|
|
1570
|
+
env: opts.runtime?.__advisorTest?.gateEnv,
|
|
1571
|
+
fetchImpl: opts.runtime?.__advisorTest?.gateFetch,
|
|
1572
|
+
signal: opts.signal,
|
|
1573
|
+
});
|
|
1574
|
+
gateRecord = gateRecordFrom(gateDecision);
|
|
1575
|
+
}
|
|
1576
|
+
// Profiles resolve for every consultation: with discovery the advisor
|
|
1577
|
+
// policy reflects live variants, otherwise it fixes to the configured
|
|
1578
|
+
// effort (or the caller's pin).
|
|
1579
|
+
advisorProfile ??= resolveAdvisorProfile({
|
|
1580
|
+
providerID: config.provider,
|
|
1581
|
+
modelID: config.model,
|
|
1582
|
+
requestedEffort,
|
|
1583
|
+
supportedEfforts: [],
|
|
1584
|
+
defaultEffort: config.variant ?? null,
|
|
1585
|
+
});
|
|
1586
|
+
opts.runtime?.__advisorTest?.profileSink?.({
|
|
1587
|
+
requester: requesterProfile,
|
|
1588
|
+
advisor: advisorProfile,
|
|
1589
|
+
});
|
|
1590
|
+
if (gateDecision?.status === "skip") {
|
|
1591
|
+
const latencyMs = Date.now() - started;
|
|
1592
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1593
|
+
ts: new Date().toISOString(),
|
|
1594
|
+
sessionId,
|
|
1595
|
+
callerModel,
|
|
1596
|
+
callerAgent,
|
|
1597
|
+
directory,
|
|
1598
|
+
mode,
|
|
1599
|
+
trigger,
|
|
1600
|
+
questionChars,
|
|
1601
|
+
effort: null,
|
|
1602
|
+
outcome: "skipped_typesafe",
|
|
1603
|
+
errorType: null,
|
|
1604
|
+
latencyMs,
|
|
1605
|
+
inputTokens: null,
|
|
1606
|
+
outputTokens: null,
|
|
1607
|
+
transcriptChars: transcript.length,
|
|
1608
|
+
priorConsultations: prior.count,
|
|
1609
|
+
via: "opencode-session",
|
|
1610
|
+
gate: gateRecord,
|
|
1611
|
+
benchmarks: summarizeBenchmarkEvidence(benchmarkLoaded, null),
|
|
1612
|
+
});
|
|
1613
|
+
console.log(`[advisor] session=${sessionId} mode=${mode} trigger=${trigger} outcome=skipped_typesafe needed=${gateDecision.metrics.neededProbability ?? "n/a"} latencyMs=${latencyMs}`);
|
|
1614
|
+
const skipNote = gateSummary(gateRecord);
|
|
1615
|
+
return (`advisor consultation skipped (typesafe): the request did not need an advisor at this point` +
|
|
1616
|
+
`${gateDecision.metrics.neededProbability !== null ? ` (need probability ${gateDecision.metrics.neededProbability.toFixed(2)})` : ""}. ` +
|
|
1617
|
+
`Ask again with a concrete unresolved question if the situation changes.` +
|
|
1618
|
+
(skipNote ? `\n_${skipNote}_` : ""));
|
|
1619
|
+
}
|
|
1620
|
+
effectiveEffort =
|
|
1621
|
+
gateDecision?.status === "proceed" && gateDecision.effectiveEffort
|
|
1622
|
+
? gateDecision.effectiveEffort
|
|
1623
|
+
: requestedEffort;
|
|
1145
1624
|
const result = await callAdvisor({
|
|
1146
1625
|
runtime: opts.runtime,
|
|
1147
1626
|
systemPrompt,
|
|
1148
1627
|
transcript,
|
|
1149
1628
|
question: opts.question,
|
|
1150
1629
|
priorNote,
|
|
1151
|
-
effort:
|
|
1630
|
+
effort: effectiveEffort,
|
|
1152
1631
|
signal: opts.signal,
|
|
1153
1632
|
config,
|
|
1154
1633
|
});
|
|
1155
1634
|
const latencyMs = Date.now() - started;
|
|
1156
|
-
await logAdvisorMetrics({
|
|
1635
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1157
1636
|
ts: new Date().toISOString(),
|
|
1158
1637
|
sessionId,
|
|
1159
1638
|
callerModel,
|
|
@@ -1171,16 +1650,20 @@ async function runAdvisor(opts) {
|
|
|
1171
1650
|
transcriptChars: transcript.length,
|
|
1172
1651
|
priorConsultations: prior.count,
|
|
1173
1652
|
via: "opencode-session",
|
|
1653
|
+
gate: gateRecord,
|
|
1654
|
+
benchmarks: summarizeBenchmarkEvidence(benchmarkLoaded, result.variant ?? effectiveEffort ?? requestedEffort ?? null),
|
|
1174
1655
|
});
|
|
1175
1656
|
console.log(`[advisor] session=${sessionId} mode=${mode} trigger=${trigger} outcome=advisor_response latencyMs=${latencyMs}`);
|
|
1657
|
+
const gateNote = gateSummary(gateRecord);
|
|
1176
1658
|
return (result.text +
|
|
1177
|
-
`\n\n_advisor consultation #${prior.count + 1} in this session chain (trigger=${trigger})_`
|
|
1659
|
+
`\n\n_advisor consultation #${prior.count + 1} in this session chain (trigger=${trigger})_` +
|
|
1660
|
+
(gateNote ? `\n_${gateNote}_` : ""));
|
|
1178
1661
|
}
|
|
1179
1662
|
catch (err) {
|
|
1180
1663
|
const message = err instanceof Error ? err.message : String(err);
|
|
1181
1664
|
const errorType = classifyAdvisorError(message);
|
|
1182
1665
|
const latencyMs = Date.now() - started;
|
|
1183
|
-
await logAdvisorMetrics({
|
|
1666
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1184
1667
|
ts: new Date().toISOString(),
|
|
1185
1668
|
sessionId,
|
|
1186
1669
|
callerModel,
|
|
@@ -1189,7 +1672,7 @@ async function runAdvisor(opts) {
|
|
|
1189
1672
|
mode,
|
|
1190
1673
|
trigger,
|
|
1191
1674
|
questionChars,
|
|
1192
|
-
effort: requestedEffort ?? null,
|
|
1675
|
+
effort: effectiveEffort ?? requestedEffort ?? null,
|
|
1193
1676
|
outcome: "error",
|
|
1194
1677
|
errorType,
|
|
1195
1678
|
latencyMs,
|
|
@@ -1198,6 +1681,8 @@ async function runAdvisor(opts) {
|
|
|
1198
1681
|
transcriptChars: transcript.length,
|
|
1199
1682
|
priorConsultations: prior.count,
|
|
1200
1683
|
via: "opencode-session",
|
|
1684
|
+
gate: gateRecord,
|
|
1685
|
+
benchmarks: summarizeBenchmarkEvidence(benchmarkLoaded, effectiveEffort ?? requestedEffort ?? null),
|
|
1201
1686
|
});
|
|
1202
1687
|
console.log(`[advisor] session=${sessionId} mode=${mode} trigger=${trigger} outcome=error errorType=${errorType} latencyMs=${latencyMs}`);
|
|
1203
1688
|
throw new Error(`advisor failed (${errorType}): ${message}`);
|
|
@@ -1236,6 +1721,8 @@ export async function setupOcAdvisorV2(ctx) {
|
|
|
1236
1721
|
signal: context.abort,
|
|
1237
1722
|
callerAgent: context.agent,
|
|
1238
1723
|
callerDirectory: context.directory,
|
|
1724
|
+
callerMessageID: context.messageID,
|
|
1725
|
+
callerCallID: context.id,
|
|
1239
1726
|
config: advisorConfig,
|
|
1240
1727
|
});
|
|
1241
1728
|
return { content: text };
|
|
@@ -1251,15 +1738,17 @@ export async function setupOcAdvisorV2(ctx) {
|
|
|
1251
1738
|
return;
|
|
1252
1739
|
// `event.tools` lists the direct tools available to this request.
|
|
1253
1740
|
// OpenCode drops entries a hook adds for tools it did not register,
|
|
1254
|
-
// so the hook can only hide the tool (Fable sessions),
|
|
1255
|
-
// The checkpoint instruction is injected only when
|
|
1256
|
-
// actually available, e.g. not when a permission rule
|
|
1741
|
+
// so the hook can only hide the tool (Fable or opted-out sessions),
|
|
1742
|
+
// never add it. The checkpoint instruction is injected only when
|
|
1743
|
+
// the tool is actually available, e.g. not when a permission rule
|
|
1744
|
+
// removed it.
|
|
1745
|
+
const disabled = advisorDisabledReason(event.model, advisorConfig);
|
|
1257
1746
|
let available = false;
|
|
1258
1747
|
for (const key of Object.keys(event.tools)) {
|
|
1259
1748
|
if (!isAdvisorToolName(key)) {
|
|
1260
1749
|
continue;
|
|
1261
1750
|
}
|
|
1262
|
-
if (
|
|
1751
|
+
if (disabled) {
|
|
1263
1752
|
delete event.tools[key];
|
|
1264
1753
|
}
|
|
1265
1754
|
else {
|
|
@@ -1292,5 +1781,5 @@ export const OcAdvisorPluginV2 = plugin;
|
|
|
1292
1781
|
export default plugin;
|
|
1293
1782
|
// Named exports for unit tests (bun test). The plugin entrypoint is the
|
|
1294
1783
|
// default export above.
|
|
1295
|
-
export { ADVISOR_TRIGGERS, CHECKPOINT_INSTRUCTION, DEFAULT_ADVISOR_CONFIG, TOOL_DESCRIPTION, buildAdvisorInputSchema, buildAdvisorPrompt, buildToolDescription, checkAdvisorSupport, classifyAdvisorError, ensureAdvisorSession, extractGeneratedText, findAdvisorModel, hasAdvisorConnection, inferTrigger, isAdvisorToolName, isFableModel, isProviderUsable, parseModelRef, resolveAdvisorConfig, resolveAdvisorVariant, resolveRequestedEffort, resetAdvisorSessionCache, unwrapData, withTimeout, };
|
|
1784
|
+
export { ADVISOR_TRIGGERS, CHECKPOINT_INSTRUCTION, DEFAULT_ADVISOR_CONFIG, TOOL_DESCRIPTION, buildAdvisorInputSchema, buildAdvisorPrompt, buildToolDescription, checkAdvisorSupport, classifyAdvisorError, ensureAdvisorSession, extractGeneratedText, findAdvisorModel, hasAdvisorConnection, advisorDisabledReason, inferTrigger, isAdvisorToolName, isFableModel, isProviderUsable, parseModelRef, resolveAdvisorConfig, resolveAdvisorVariant, resolveRequestedEffort, resolveSupportedEfforts, resetAdvisorSessionCache, resetBenchmarkStores, unwrapData, withTimeout, formatV2Message, runAdvisor, };
|
|
1296
1785
|
//# sourceMappingURL=ocAdvisor.js.map
|