@pfoundation/ocadvisor 26.9.1 → 26.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +388 -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 +19 -0
- package/dist/benchmarkConfig.d.ts.map +1 -0
- package/dist/benchmarkConfig.js +58 -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 +57 -0
- package/dist/benchmarkMatch.d.ts.map +1 -0
- package/dist/benchmarkMatch.js +236 -0
- package/dist/benchmarkMatch.js.map +1 -0
- package/dist/benchmarkStore.d.ts +44 -0
- package/dist/benchmarkStore.d.ts.map +1 -0
- package/dist/benchmarkStore.js +308 -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 +377 -0
- package/dist/cli.js.map +1 -0
- package/dist/data/artificialAnalysis.mappings.json +197 -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 +502 -44
- package/dist/ocAdvisor.js.map +1 -1
- package/dist/typesafeGate.d.ts +89 -0
- package/dist/typesafeGate.d.ts.map +1 -0
- package/dist/typesafeGate.js +463 -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,59 @@ 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 }` with absolute locations. `undefined`/`null`
|
|
111
|
+
// means "not set"; blank entries are dropped; unknown keys are ignored.
|
|
112
|
+
// Invalid explicit values throw so misconfiguration fails fast at setup
|
|
113
|
+
// instead of silently degrading to default benchmark data.
|
|
114
|
+
function normalizeBenchmarks(value) {
|
|
115
|
+
if (value === undefined || value === null)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
118
|
+
throw new Error("benchmarks must be an object with path options.");
|
|
119
|
+
}
|
|
120
|
+
const raw = value;
|
|
121
|
+
const out = {};
|
|
122
|
+
for (const key of ["path", "mappingsPath"]) {
|
|
123
|
+
const entry = raw[key];
|
|
124
|
+
if (entry === undefined || entry === null)
|
|
125
|
+
continue;
|
|
126
|
+
if (typeof entry !== "string") {
|
|
127
|
+
throw new Error(`benchmarks.${key} must be an absolute path.`);
|
|
128
|
+
}
|
|
129
|
+
const trimmed = entry.trim();
|
|
130
|
+
if (!trimmed)
|
|
131
|
+
continue;
|
|
132
|
+
if (!isAbsolute(trimmed)) {
|
|
133
|
+
throw new Error(`benchmarks.${key} must be an absolute path.`);
|
|
134
|
+
}
|
|
135
|
+
out[key] = trimmed;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
75
139
|
function toBoundedInt(value, min) {
|
|
76
140
|
const raw = typeof value === "number"
|
|
77
141
|
? value
|
|
@@ -108,7 +172,7 @@ function parseModelRef(ref) {
|
|
|
108
172
|
}
|
|
109
173
|
return out;
|
|
110
174
|
}
|
|
111
|
-
function applyAdvisorConfigSource(base, src) {
|
|
175
|
+
function applyAdvisorConfigSource(base, src, env = process.env) {
|
|
112
176
|
if (!src || typeof src !== "object")
|
|
113
177
|
return base;
|
|
114
178
|
const next = { ...base };
|
|
@@ -135,12 +199,31 @@ function applyAdvisorConfigSource(base, src) {
|
|
|
135
199
|
if (normalized !== undefined)
|
|
136
200
|
next.agentEffort = normalized;
|
|
137
201
|
}
|
|
202
|
+
const disabledForModels = normalizeDisabledForModels(src.disabledForModels);
|
|
203
|
+
if (disabledForModels !== undefined) {
|
|
204
|
+
next.disabledForModels = disabledForModels;
|
|
205
|
+
}
|
|
206
|
+
// Benchmark paths merge per field so a plugin option can override one
|
|
207
|
+
// location while the other still falls back to the environment default.
|
|
208
|
+
const benchmarks = normalizeBenchmarks(src.benchmarks);
|
|
209
|
+
if (benchmarks !== undefined) {
|
|
210
|
+
next.benchmarks = { ...next.benchmarks, ...benchmarks };
|
|
211
|
+
}
|
|
138
212
|
const timeout = toBoundedInt(src.timeoutMs ?? src.timeout_ms, 1);
|
|
139
213
|
if (timeout !== undefined)
|
|
140
214
|
next.timeoutMs = timeout;
|
|
141
215
|
const cap = toBoundedInt(src.maxTranscriptChars ?? src.max_transcript_chars, 0);
|
|
142
216
|
if (cap !== undefined)
|
|
143
217
|
next.maxTranscriptChars = cap;
|
|
218
|
+
if (src.typesafe !== undefined) {
|
|
219
|
+
const normalized = normalizeTypeSafeOptions(src.typesafe);
|
|
220
|
+
if ("error" in normalized) {
|
|
221
|
+
throw new Error(normalized.error);
|
|
222
|
+
}
|
|
223
|
+
next.typesafeSource = normalized;
|
|
224
|
+
}
|
|
225
|
+
next.typesafe = resolveTypeSafeConfig(next.typesafeSource, env);
|
|
226
|
+
next.typesafeSettings = next.typesafe.settings;
|
|
144
227
|
return next;
|
|
145
228
|
}
|
|
146
229
|
function envAdvisorConfigSource(env = process.env) {
|
|
@@ -151,13 +234,21 @@ function envAdvisorConfigSource(env = process.env) {
|
|
|
151
234
|
timeoutMs: env.OCADVISOR_TIMEOUT_MS,
|
|
152
235
|
maxTranscriptChars: env.OCADVISOR_MAX_TRANSCRIPT_CHARS,
|
|
153
236
|
agentEffort: env.OCADVISOR_AGENT_EFFORT,
|
|
237
|
+
disabledForModels: env.OCADVISOR_DISABLED_FOR_MODELS,
|
|
238
|
+
benchmarks: env.OCADVISOR_BENCHMARKS_PATH === undefined &&
|
|
239
|
+
env.OCADVISOR_BENCHMARK_MAPPINGS_PATH === undefined
|
|
240
|
+
? undefined
|
|
241
|
+
: {
|
|
242
|
+
path: env.OCADVISOR_BENCHMARKS_PATH,
|
|
243
|
+
mappingsPath: env.OCADVISOR_BENCHMARK_MAPPINGS_PATH,
|
|
244
|
+
},
|
|
154
245
|
};
|
|
155
246
|
}
|
|
156
247
|
// Precedence (low to high): built-in defaults, environment variables,
|
|
157
248
|
// plugin options from opencode.json.
|
|
158
249
|
function resolveAdvisorConfig(options, env = process.env) {
|
|
159
|
-
let config = applyAdvisorConfigSource(DEFAULT_ADVISOR_CONFIG, envAdvisorConfigSource(env));
|
|
160
|
-
config = applyAdvisorConfigSource(config, options);
|
|
250
|
+
let config = applyAdvisorConfigSource(DEFAULT_ADVISOR_CONFIG, envAdvisorConfigSource(env), env);
|
|
251
|
+
config = applyAdvisorConfigSource(config, options, env);
|
|
161
252
|
return config;
|
|
162
253
|
}
|
|
163
254
|
const SYSTEM_BASE = `You are a senior advisor reviewing a coding agent's work. You have the full session transcript.
|
|
@@ -174,15 +265,17 @@ You are a debugger. Analyze error patterns, stack traces, and failed attempts in
|
|
|
174
265
|
};
|
|
175
266
|
const TOOL_DESCRIPTION = `Consult a senior advisor model with your full session transcript — including parent sessions for subagents — for high-quality analysis.
|
|
176
267
|
|
|
177
|
-
Use advisor
|
|
268
|
+
Use advisor when an independent perspective could improve the approach, help resolve a problem, or strengthen an implementation review.
|
|
269
|
+
|
|
270
|
+
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
271
|
|
|
179
|
-
|
|
180
|
-
- mode "general": a second opinion that does not fit the above.
|
|
272
|
+
Ask a concrete, focused question. Avoid repeating settled questions without new context. Straightforward tasks usually need no consultation.
|
|
181
273
|
|
|
182
274
|
Rules:
|
|
183
275
|
- Always pass a concrete "question" naming the decision or artifact under review.
|
|
184
|
-
-
|
|
276
|
+
- Reconcile an advisor conflict with primary-source evidence via one "followup" call stating both sides.
|
|
185
277
|
- 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.
|
|
278
|
+
- 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
279
|
|
|
187
280
|
Args: "mode" (general, review, plan, debug), "trigger" (before_approach, stuck, pre_complete, followup, other), "question" (concrete question focusing the advisor).
|
|
188
281
|
`;
|
|
@@ -195,7 +288,7 @@ function buildToolDescription(config = DEFAULT_ADVISOR_CONFIG) {
|
|
|
195
288
|
return (TOOL_DESCRIPTION +
|
|
196
289
|
`Optional "effort" (one of: ${config.agentEffort.join(", ")}): reasoning effort for this consultation; omit to use the configured variant.\n`);
|
|
197
290
|
}
|
|
198
|
-
const CHECKPOINT_INSTRUCTION = `[advisor]
|
|
291
|
+
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
292
|
const ADVISOR_TRIGGERS = [
|
|
200
293
|
"before_approach",
|
|
201
294
|
"stuck",
|
|
@@ -240,8 +333,10 @@ function buildAdvisorInputSchema(config = DEFAULT_ADVISOR_CONFIG) {
|
|
|
240
333
|
},
|
|
241
334
|
};
|
|
242
335
|
}
|
|
243
|
-
function openDb() {
|
|
244
|
-
return new Database(DB_PATH, {
|
|
336
|
+
function openDb(runtime) {
|
|
337
|
+
return new Database(runtime?.__advisorTest?.dbPath ?? DB_PATH, {
|
|
338
|
+
readonly: true,
|
|
339
|
+
});
|
|
245
340
|
}
|
|
246
341
|
function tableExists(db, name) {
|
|
247
342
|
const row = db
|
|
@@ -267,6 +362,22 @@ function isFableModel(model) {
|
|
|
267
362
|
const id = String(model.id || model.modelID || "").toLowerCase();
|
|
268
363
|
return provider.includes("anthropic") && id.includes("fable");
|
|
269
364
|
}
|
|
365
|
+
function advisorDisabledReason(model, config) {
|
|
366
|
+
if (isFableModel(model)) {
|
|
367
|
+
return { outcome: "skipped_fable", message: FABLE_DISABLED };
|
|
368
|
+
}
|
|
369
|
+
const provider = model?.providerID || model?.provider;
|
|
370
|
+
const id = model?.id || model?.modelID;
|
|
371
|
+
if (!provider || !id)
|
|
372
|
+
return null;
|
|
373
|
+
const ref = `${provider}/${id}`;
|
|
374
|
+
if (!config.disabledForModels.includes(ref))
|
|
375
|
+
return null;
|
|
376
|
+
return {
|
|
377
|
+
outcome: "skipped_model",
|
|
378
|
+
message: `advisor is disabled (model opt-out): ${ref} is listed in disabledForModels.`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
270
381
|
function inferTrigger(mode, trigger) {
|
|
271
382
|
if (trigger && ADVISOR_TRIGGERS.includes(trigger)) {
|
|
272
383
|
return trigger;
|
|
@@ -405,6 +516,13 @@ function isAdvisorToolName(name) {
|
|
|
405
516
|
function countPriorAdvisorCalls(db, sessionId) {
|
|
406
517
|
const callIds = new Set();
|
|
407
518
|
const modes = [];
|
|
519
|
+
const questions = [];
|
|
520
|
+
const record = (input) => {
|
|
521
|
+
modes.push(String(input?.mode || "general"));
|
|
522
|
+
if (typeof input?.question === "string" && input.question.trim()) {
|
|
523
|
+
questions.push(input.question);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
408
526
|
try {
|
|
409
527
|
for (const sid of collectSessionChain(db, sessionId)) {
|
|
410
528
|
if (tableExists(db, "session_message")) {
|
|
@@ -433,7 +551,7 @@ function countPriorAdvisorCalls(db, sessionId) {
|
|
|
433
551
|
const cid = String(block.id || block.callID || row.id);
|
|
434
552
|
if (!callIds.has(cid)) {
|
|
435
553
|
callIds.add(cid);
|
|
436
|
-
|
|
554
|
+
record(state.input);
|
|
437
555
|
}
|
|
438
556
|
}
|
|
439
557
|
nested.forEach((call, index) => {
|
|
@@ -441,7 +559,7 @@ function countPriorAdvisorCalls(db, sessionId) {
|
|
|
441
559
|
const cid = `${block.id || row.id}#${index}`;
|
|
442
560
|
if (!callIds.has(cid)) {
|
|
443
561
|
callIds.add(cid);
|
|
444
|
-
|
|
562
|
+
record(call.input);
|
|
445
563
|
}
|
|
446
564
|
}
|
|
447
565
|
});
|
|
@@ -469,22 +587,88 @@ function countPriorAdvisorCalls(db, sessionId) {
|
|
|
469
587
|
if (!cid || callIds.has(cid))
|
|
470
588
|
continue;
|
|
471
589
|
callIds.add(cid);
|
|
472
|
-
|
|
590
|
+
record(block.state?.input);
|
|
473
591
|
}
|
|
474
592
|
}
|
|
475
593
|
}
|
|
476
594
|
}
|
|
477
595
|
catch { }
|
|
478
|
-
return { count: callIds.size, modes };
|
|
596
|
+
return { count: callIds.size, modes, questions };
|
|
479
597
|
}
|
|
480
|
-
async function logAdvisorMetrics(metrics) {
|
|
598
|
+
async function logAdvisorMetrics(runtime, metrics) {
|
|
481
599
|
try {
|
|
482
|
-
|
|
600
|
+
const path = runtime?.__advisorTest?.metricsPath ?? METRICS_PATH;
|
|
601
|
+
await appendFile(path, JSON.stringify(metrics) + "\n", "utf-8");
|
|
483
602
|
}
|
|
484
603
|
catch {
|
|
485
604
|
// Metrics must never break the advisor call.
|
|
486
605
|
}
|
|
487
606
|
}
|
|
607
|
+
// Flattens a gate decision into the additive metrics record. A bypassed gate
|
|
608
|
+
// reports `bypass` so reports can separate bypass, skip, fallback, and
|
|
609
|
+
// allowed proceed without inferring from other fields.
|
|
610
|
+
function gateRecordFrom(decision) {
|
|
611
|
+
if (decision.status === "disabled") {
|
|
612
|
+
return {
|
|
613
|
+
status: "bypass",
|
|
614
|
+
reason: "disabled",
|
|
615
|
+
model: null,
|
|
616
|
+
neededProbability: null,
|
|
617
|
+
suggestedEffort: null,
|
|
618
|
+
effortConfidence: null,
|
|
619
|
+
effectiveEffort: null,
|
|
620
|
+
effortSource: null,
|
|
621
|
+
latencyMs: 0,
|
|
622
|
+
stateBytes: 0,
|
|
623
|
+
truncated: false,
|
|
624
|
+
inputTokens: null,
|
|
625
|
+
outputTokens: null,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const metrics = decision.metrics;
|
|
629
|
+
return {
|
|
630
|
+
status: decision.status,
|
|
631
|
+
reason: decision.reason,
|
|
632
|
+
model: metrics.model,
|
|
633
|
+
neededProbability: metrics.neededProbability,
|
|
634
|
+
suggestedEffort: metrics.suggestedEffort,
|
|
635
|
+
effortConfidence: metrics.effortConfidence,
|
|
636
|
+
effectiveEffort: decision.status === "proceed" ? decision.effectiveEffort : null,
|
|
637
|
+
effortSource: decision.status === "proceed" ? decision.effortSource : null,
|
|
638
|
+
latencyMs: metrics.latencyMs,
|
|
639
|
+
stateBytes: metrics.stateBytes,
|
|
640
|
+
truncated: metrics.truncated,
|
|
641
|
+
inputTokens: metrics.inputTokens,
|
|
642
|
+
outputTokens: metrics.outputTokens,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
// Human-readable gate summary for the tool output. A bypassed gate contributes
|
|
646
|
+
// nothing, so keyless or disabled installs keep the plain footer.
|
|
647
|
+
export function gateSummary(gate) {
|
|
648
|
+
if (!gate || gate.status === "bypass")
|
|
649
|
+
return null;
|
|
650
|
+
const parts = [];
|
|
651
|
+
if (gate.neededProbability !== null) {
|
|
652
|
+
parts.push(`need=${gate.neededProbability.toFixed(2)}`);
|
|
653
|
+
}
|
|
654
|
+
if (gate.effectiveEffort) {
|
|
655
|
+
parts.push(gate.effortSource === "typesafe"
|
|
656
|
+
? `effort=${gate.effectiveEffort} (gate)`
|
|
657
|
+
: `effort=${gate.effectiveEffort}`);
|
|
658
|
+
}
|
|
659
|
+
switch (gate.status) {
|
|
660
|
+
case "skip":
|
|
661
|
+
parts.push("decision=skip", gate.reason);
|
|
662
|
+
break;
|
|
663
|
+
case "fallback":
|
|
664
|
+
parts.push("decision=fallback", gate.reason);
|
|
665
|
+
break;
|
|
666
|
+
default:
|
|
667
|
+
parts.push("decision=proceed");
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
return `gate: ${parts.join(", ")}`;
|
|
671
|
+
}
|
|
488
672
|
function getSession(db, sessionId) {
|
|
489
673
|
return db
|
|
490
674
|
.query("SELECT id, parent_id, title, directory FROM session WHERE id = ?")
|
|
@@ -759,6 +943,20 @@ function resolveAdvisorVariant(model, config = DEFAULT_ADVISOR_CONFIG, requested
|
|
|
759
943
|
const ids = model.variants.map((variant) => variant?.id);
|
|
760
944
|
return ids.includes(config.variant) ? config.variant : undefined;
|
|
761
945
|
}
|
|
946
|
+
// Effort levels the gate may choose: plugin-allowed candidates intersected
|
|
947
|
+
// with the model's live variants. Unknown discovery yields no candidates,
|
|
948
|
+
// which keeps the configured effort and omits automatic effort selection.
|
|
949
|
+
function resolveSupportedEfforts(model, config) {
|
|
950
|
+
const allowed = config.agentEffort && config.agentEffort.length > 0
|
|
951
|
+
? config.agentEffort
|
|
952
|
+
: null;
|
|
953
|
+
if (!allowed || !model || !Array.isArray(model.variants))
|
|
954
|
+
return [];
|
|
955
|
+
const variants = new Set(model.variants
|
|
956
|
+
.map((variant) => variant?.id)
|
|
957
|
+
.filter((id) => !!id));
|
|
958
|
+
return allowed.filter((effort) => variants.has(effort));
|
|
959
|
+
}
|
|
762
960
|
function hasAdvisorConnection(connection) {
|
|
763
961
|
if (connection === null || connection === undefined)
|
|
764
962
|
return false;
|
|
@@ -772,11 +970,50 @@ function hasAdvisorConnection(connection) {
|
|
|
772
970
|
}
|
|
773
971
|
return true;
|
|
774
972
|
}
|
|
973
|
+
// Discovery namespaces: the documented V2 `model`/`provider` domains win,
|
|
974
|
+
// the legacy `catalog` namespace stays for older servers and fixtures.
|
|
975
|
+
// Calls stay in method form so any receiver-bound implementation keeps
|
|
976
|
+
// working.
|
|
977
|
+
function discoveryModelApi(runtime) {
|
|
978
|
+
if (runtime.model && typeof runtime.model.list === "function") {
|
|
979
|
+
return runtime.model;
|
|
980
|
+
}
|
|
981
|
+
if (runtime.catalog?.model &&
|
|
982
|
+
typeof runtime.catalog.model.list === "function") {
|
|
983
|
+
return runtime.catalog.model;
|
|
984
|
+
}
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
function discoveryProviderApi(runtime) {
|
|
988
|
+
if (runtime.provider && typeof runtime.provider.get === "function") {
|
|
989
|
+
return runtime.provider;
|
|
990
|
+
}
|
|
991
|
+
if (runtime.catalog?.provider &&
|
|
992
|
+
typeof runtime.catalog.provider.get === "function") {
|
|
993
|
+
return runtime.catalog.provider;
|
|
994
|
+
}
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
// Shared by support checks and the effort gate; null means discovery is
|
|
998
|
+
// unavailable or the configured model was not found.
|
|
999
|
+
async function findAdvisorCatalogModel(runtime, config) {
|
|
1000
|
+
const api = discoveryModelApi(runtime);
|
|
1001
|
+
if (!api)
|
|
1002
|
+
return null;
|
|
1003
|
+
try {
|
|
1004
|
+
const models = unwrapData((await api.list()));
|
|
1005
|
+
return findAdvisorModel(models, config);
|
|
1006
|
+
}
|
|
1007
|
+
catch {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
775
1011
|
async function checkAdvisorSupport(runtime, config = DEFAULT_ADVISOR_CONFIG, requestedVariant) {
|
|
776
|
-
|
|
1012
|
+
const providerApi = discoveryProviderApi(runtime);
|
|
1013
|
+
if (providerApi) {
|
|
777
1014
|
let provider = null;
|
|
778
1015
|
try {
|
|
779
|
-
provider = unwrapData((await
|
|
1016
|
+
provider = unwrapData((await providerApi.get({
|
|
780
1017
|
providerID: config.provider,
|
|
781
1018
|
})));
|
|
782
1019
|
}
|
|
@@ -794,10 +1031,11 @@ async function checkAdvisorSupport(runtime, config = DEFAULT_ADVISOR_CONFIG, req
|
|
|
794
1031
|
}
|
|
795
1032
|
}
|
|
796
1033
|
let variant = requestedVariant ?? config.variant;
|
|
797
|
-
|
|
1034
|
+
const modelApi = discoveryModelApi(runtime);
|
|
1035
|
+
if (modelApi) {
|
|
798
1036
|
let models = null;
|
|
799
1037
|
try {
|
|
800
|
-
models = unwrapData((await
|
|
1038
|
+
models = unwrapData((await modelApi.list()));
|
|
801
1039
|
}
|
|
802
1040
|
catch (err) {
|
|
803
1041
|
return {
|
|
@@ -1025,10 +1263,25 @@ async function callAdvisor(opts) {
|
|
|
1025
1263
|
const text = await enqueueAdvisor(() => withTimeout((async () => {
|
|
1026
1264
|
const sessionId = await ensureAdvisorSession(runtime, support.variant, config);
|
|
1027
1265
|
const request = { sessionID: sessionId, prompt };
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1266
|
+
const startedAt = Date.now();
|
|
1267
|
+
const run = async () => {
|
|
1268
|
+
const result = opts.signal
|
|
1269
|
+
? await generate(request, { signal: opts.signal })
|
|
1270
|
+
: await generate(request);
|
|
1271
|
+
return extractGeneratedText(result);
|
|
1272
|
+
};
|
|
1273
|
+
// A generation can come back without text (transient provider
|
|
1274
|
+
// behavior, e.g. a reasoning-only response with adaptive thinking).
|
|
1275
|
+
// Retry once while most of the timeout budget remains; never retry
|
|
1276
|
+
// after a caller cancellation.
|
|
1277
|
+
let output = await run();
|
|
1278
|
+
if (!output?.trim() && !opts.signal?.aborted) {
|
|
1279
|
+
const elapsed = Date.now() - startedAt;
|
|
1280
|
+
if (elapsed < config.timeoutMs / 2) {
|
|
1281
|
+
console.log(`[advisor] empty generation; retrying once (session=${sessionId} elapsedMs=${elapsed})`);
|
|
1282
|
+
output = await run();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1032
1285
|
if (!output?.trim()) {
|
|
1033
1286
|
throw new Error("Advisor returned an empty response.");
|
|
1034
1287
|
}
|
|
@@ -1044,6 +1297,85 @@ async function callAdvisor(opts) {
|
|
|
1044
1297
|
variant: support.variant,
|
|
1045
1298
|
};
|
|
1046
1299
|
}
|
|
1300
|
+
// Benchmark stores live for the process, keyed by resolved user paths (and
|
|
1301
|
+
// test seed overrides), so every consultation observes CLI refreshes
|
|
1302
|
+
// without re-reading unchanged files. Tests reset between cases.
|
|
1303
|
+
const benchmarkStores = new Map();
|
|
1304
|
+
function resetBenchmarkStores() {
|
|
1305
|
+
benchmarkStores.clear();
|
|
1306
|
+
}
|
|
1307
|
+
function benchmarkStoreKey(snapshotPath, mappingsPath, seedSnapshotPath, seedMappingsPath) {
|
|
1308
|
+
return [
|
|
1309
|
+
snapshotPath,
|
|
1310
|
+
mappingsPath,
|
|
1311
|
+
seedSnapshotPath ?? "",
|
|
1312
|
+
seedMappingsPath ?? "",
|
|
1313
|
+
].join("\n");
|
|
1314
|
+
}
|
|
1315
|
+
// Loads one consistent benchmark view and assembles gate evidence from it.
|
|
1316
|
+
// Any failure (bad paths, unreadable files, unexpected errors) yields null
|
|
1317
|
+
// and the consultation proceeds without benchmark enrichment.
|
|
1318
|
+
async function loadBenchmarkEvidence(runtime, config, requester, advisor) {
|
|
1319
|
+
try {
|
|
1320
|
+
const testOpts = runtime?.__advisorTest?.benchmarkStoreOptions;
|
|
1321
|
+
const paths = resolveBenchmarkPaths(config.benchmarks, testOpts?.env ?? process.env);
|
|
1322
|
+
if ("error" in paths)
|
|
1323
|
+
return null;
|
|
1324
|
+
const key = benchmarkStoreKey(paths.snapshotPath, paths.mappingsPath, testOpts?.seedSnapshotPath, testOpts?.seedMappingsPath);
|
|
1325
|
+
let store = benchmarkStores.get(key);
|
|
1326
|
+
if (!store) {
|
|
1327
|
+
store = await createBenchmarkStore({
|
|
1328
|
+
snapshotPath: paths.snapshotPath,
|
|
1329
|
+
mappingsPath: paths.mappingsPath,
|
|
1330
|
+
seedSnapshotPath: testOpts?.seedSnapshotPath,
|
|
1331
|
+
seedMappingsPath: testOpts?.seedMappingsPath,
|
|
1332
|
+
fs: testOpts?.fs,
|
|
1333
|
+
maxBytes: testOpts?.maxBytes,
|
|
1334
|
+
});
|
|
1335
|
+
benchmarkStores.set(key, store);
|
|
1336
|
+
}
|
|
1337
|
+
const view = await store.view();
|
|
1338
|
+
return {
|
|
1339
|
+
view,
|
|
1340
|
+
evidence: buildBenchmarkEvidence({ requester, advisor, view }),
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
catch {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function summarizeBenchmarkEvidence(loaded, finalEffort) {
|
|
1348
|
+
if (!loaded)
|
|
1349
|
+
return undefined;
|
|
1350
|
+
const { view, evidence } = loaded;
|
|
1351
|
+
const { requester, advisor } = evidence.models;
|
|
1352
|
+
const policy = advisor.policy;
|
|
1353
|
+
const finalMatch = finalEffort === null
|
|
1354
|
+
? null
|
|
1355
|
+
: view.matcher.match({
|
|
1356
|
+
providerID: advisor.providerID,
|
|
1357
|
+
modelID: advisor.modelID,
|
|
1358
|
+
variant: finalEffort,
|
|
1359
|
+
}).status;
|
|
1360
|
+
return {
|
|
1361
|
+
source: evidence.benchmarks.source,
|
|
1362
|
+
contentHash: evidence.benchmarks.contentHash,
|
|
1363
|
+
fetchedAt: evidence.benchmarks.fetchedAt,
|
|
1364
|
+
hashVerified: evidence.benchmarks.hashVerified,
|
|
1365
|
+
requester: requester.providerID && requester.modelID
|
|
1366
|
+
? `${requester.providerID}/${requester.modelID}${requester.variant ? `#${requester.variant}` : ""} (${requester.provenance})`
|
|
1367
|
+
: null,
|
|
1368
|
+
requesterMatch: evidence.benchmarks.requesterMatch.status,
|
|
1369
|
+
advisorPolicy: policy.kind === "pinned"
|
|
1370
|
+
? `pinned:${policy.effort}`
|
|
1371
|
+
: policy.kind === "candidates"
|
|
1372
|
+
? `candidates:${policy.candidates.join(",")}>${policy.fallback ?? "none"}`
|
|
1373
|
+
: `fixed:${policy.effort ?? "none"}`,
|
|
1374
|
+
advisorMatch: evidence.benchmarks.advisorDefaultMatch.status,
|
|
1375
|
+
finalEffort,
|
|
1376
|
+
finalMatch,
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1047
1379
|
async function runAdvisor(opts) {
|
|
1048
1380
|
const started = Date.now();
|
|
1049
1381
|
const config = opts.config ?? DEFAULT_ADVISOR_CONFIG;
|
|
@@ -1052,7 +1384,7 @@ async function runAdvisor(opts) {
|
|
|
1052
1384
|
const sessionId = opts.sessionId;
|
|
1053
1385
|
const questionChars = opts.question?.length ?? 0;
|
|
1054
1386
|
if (!sessionId) {
|
|
1055
|
-
await logAdvisorMetrics({
|
|
1387
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1056
1388
|
ts: new Date().toISOString(),
|
|
1057
1389
|
sessionId: null,
|
|
1058
1390
|
callerModel: null,
|
|
@@ -1075,13 +1407,14 @@ async function runAdvisor(opts) {
|
|
|
1075
1407
|
}
|
|
1076
1408
|
let db = null;
|
|
1077
1409
|
try {
|
|
1078
|
-
db = openDb();
|
|
1410
|
+
db = openDb(opts.runtime);
|
|
1079
1411
|
const info = getSessionInfo(db, sessionId);
|
|
1080
1412
|
const callerModel = callerLabel(info?.model ?? null);
|
|
1081
1413
|
const callerAgent = opts.callerAgent || info?.agent || null;
|
|
1082
1414
|
const directory = opts.callerDirectory || info?.directory || null;
|
|
1083
|
-
|
|
1084
|
-
|
|
1415
|
+
const disabled = advisorDisabledReason(info?.model, config);
|
|
1416
|
+
if (disabled) {
|
|
1417
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1085
1418
|
ts: new Date().toISOString(),
|
|
1086
1419
|
sessionId,
|
|
1087
1420
|
callerModel,
|
|
@@ -1091,7 +1424,7 @@ async function runAdvisor(opts) {
|
|
|
1091
1424
|
trigger,
|
|
1092
1425
|
questionChars,
|
|
1093
1426
|
effort: null,
|
|
1094
|
-
outcome:
|
|
1427
|
+
outcome: disabled.outcome,
|
|
1095
1428
|
errorType: null,
|
|
1096
1429
|
latencyMs: Date.now() - started,
|
|
1097
1430
|
inputTokens: null,
|
|
@@ -1100,12 +1433,12 @@ async function runAdvisor(opts) {
|
|
|
1100
1433
|
priorConsultations: 0,
|
|
1101
1434
|
via: "opencode-session",
|
|
1102
1435
|
});
|
|
1103
|
-
console.log(`[advisor] session=${sessionId} mode=${mode} outcome
|
|
1104
|
-
return
|
|
1436
|
+
console.log(`[advisor] session=${sessionId} mode=${mode} outcome=${disabled.outcome}`);
|
|
1437
|
+
return disabled.message;
|
|
1105
1438
|
}
|
|
1106
1439
|
let transcript = buildTranscript(db, sessionId);
|
|
1107
1440
|
if (!transcript?.trim()) {
|
|
1108
|
-
await logAdvisorMetrics({
|
|
1441
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1109
1442
|
ts: new Date().toISOString(),
|
|
1110
1443
|
sessionId,
|
|
1111
1444
|
callerModel,
|
|
@@ -1134,26 +1467,141 @@ async function runAdvisor(opts) {
|
|
|
1134
1467
|
"... (older transcript trimmed to fit maxTranscriptChars) ...\n\n" +
|
|
1135
1468
|
transcript.slice(-config.maxTranscriptChars);
|
|
1136
1469
|
}
|
|
1470
|
+
// Invocation-correct requester identity for benchmark matching: the
|
|
1471
|
+
// model that produced this tool call, resolved after the early guards
|
|
1472
|
+
// so skipped calls pay for no extra reads.
|
|
1473
|
+
const requesterProfile = resolveRequesterProfile(db, {
|
|
1474
|
+
sessionId,
|
|
1475
|
+
messageID: opts.callerMessageID,
|
|
1476
|
+
callID: opts.callerCallID,
|
|
1477
|
+
sessionModel: info?.model ?? null,
|
|
1478
|
+
});
|
|
1137
1479
|
const prior = countPriorAdvisorCalls(db, sessionId);
|
|
1138
1480
|
const priorNote = prior.count > 0
|
|
1139
1481
|
? `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
1482
|
: null;
|
|
1141
1483
|
const systemPrompt = SYSTEM_PROMPTS[mode] || SYSTEM_PROMPTS.general;
|
|
1142
1484
|
let requestedEffort;
|
|
1485
|
+
let effectiveEffort;
|
|
1486
|
+
let gateRecord;
|
|
1487
|
+
let benchmarkLoaded = null;
|
|
1143
1488
|
try {
|
|
1144
1489
|
requestedEffort = resolveRequestedEffort(config, opts.effort);
|
|
1490
|
+
// Optional TypeSafe preflight: one bounded request decides whether this
|
|
1491
|
+
// consultation is worth the expensive generation and, when the caller
|
|
1492
|
+
// did not pin an effort, which supported effort fits. Gate failures
|
|
1493
|
+
// fall back to the ordinary behavior and never fail the call.
|
|
1494
|
+
let gateDecision = null;
|
|
1495
|
+
let advisorProfile = null;
|
|
1496
|
+
if (config.typesafe.keyPresent && !config.typesafeSource.disabled) {
|
|
1497
|
+
const catalogModel = await findAdvisorCatalogModel(opts.runtime, config);
|
|
1498
|
+
const supportedEfforts = resolveSupportedEfforts(catalogModel, config);
|
|
1499
|
+
advisorProfile = resolveAdvisorProfile({
|
|
1500
|
+
providerID: config.provider,
|
|
1501
|
+
modelID: config.model,
|
|
1502
|
+
requestedEffort,
|
|
1503
|
+
supportedEfforts,
|
|
1504
|
+
defaultEffort: requestedEffort ??
|
|
1505
|
+
resolveAdvisorVariant(catalogModel, config) ??
|
|
1506
|
+
null,
|
|
1507
|
+
});
|
|
1508
|
+
const gateSettings = config.typesafe.settings ?? TYPESAFE_DEFAULTS;
|
|
1509
|
+
benchmarkLoaded = await loadBenchmarkEvidence(opts.runtime, config, requesterProfile, advisorProfile);
|
|
1510
|
+
gateDecision = await runTypeSafeGate({
|
|
1511
|
+
state: buildDecisionState(db, sessionId, {
|
|
1512
|
+
question: opts.question ?? null,
|
|
1513
|
+
mode,
|
|
1514
|
+
trigger,
|
|
1515
|
+
explicitEffort: requestedEffort ?? null,
|
|
1516
|
+
caller: callerModel,
|
|
1517
|
+
directory,
|
|
1518
|
+
priorConsultations: prior.count,
|
|
1519
|
+
priorModes: prior.modes,
|
|
1520
|
+
priorQuestions: prior.questions,
|
|
1521
|
+
supportedEfforts,
|
|
1522
|
+
defaultEffort: requestedEffort ?? config.variant ?? null,
|
|
1523
|
+
maxStateBytes: gateSettings.maxStateBytes,
|
|
1524
|
+
models: benchmarkLoaded?.evidence.models ?? null,
|
|
1525
|
+
benchmarks: benchmarkLoaded?.evidence.benchmarks ?? null,
|
|
1526
|
+
formatMessage: formatV2Message,
|
|
1527
|
+
}),
|
|
1528
|
+
input: {
|
|
1529
|
+
mode,
|
|
1530
|
+
trigger,
|
|
1531
|
+
question: opts.question ?? null,
|
|
1532
|
+
explicitEffort: requestedEffort ?? null,
|
|
1533
|
+
supportedEfforts,
|
|
1534
|
+
defaultEffort: requestedEffort ?? config.variant ?? null,
|
|
1535
|
+
},
|
|
1536
|
+
settings: gateSettings,
|
|
1537
|
+
keyPresent: config.typesafe.keyPresent,
|
|
1538
|
+
client: opts.runtime?.__advisorTest?.gateClient,
|
|
1539
|
+
env: opts.runtime?.__advisorTest?.gateEnv,
|
|
1540
|
+
fetchImpl: opts.runtime?.__advisorTest?.gateFetch,
|
|
1541
|
+
signal: opts.signal,
|
|
1542
|
+
});
|
|
1543
|
+
gateRecord = gateRecordFrom(gateDecision);
|
|
1544
|
+
}
|
|
1545
|
+
// Profiles resolve for every consultation: with discovery the advisor
|
|
1546
|
+
// policy reflects live variants, otherwise it fixes to the configured
|
|
1547
|
+
// effort (or the caller's pin).
|
|
1548
|
+
advisorProfile ??= resolveAdvisorProfile({
|
|
1549
|
+
providerID: config.provider,
|
|
1550
|
+
modelID: config.model,
|
|
1551
|
+
requestedEffort,
|
|
1552
|
+
supportedEfforts: [],
|
|
1553
|
+
defaultEffort: config.variant ?? null,
|
|
1554
|
+
});
|
|
1555
|
+
opts.runtime?.__advisorTest?.profileSink?.({
|
|
1556
|
+
requester: requesterProfile,
|
|
1557
|
+
advisor: advisorProfile,
|
|
1558
|
+
});
|
|
1559
|
+
if (gateDecision?.status === "skip") {
|
|
1560
|
+
const latencyMs = Date.now() - started;
|
|
1561
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1562
|
+
ts: new Date().toISOString(),
|
|
1563
|
+
sessionId,
|
|
1564
|
+
callerModel,
|
|
1565
|
+
callerAgent,
|
|
1566
|
+
directory,
|
|
1567
|
+
mode,
|
|
1568
|
+
trigger,
|
|
1569
|
+
questionChars,
|
|
1570
|
+
effort: null,
|
|
1571
|
+
outcome: "skipped_typesafe",
|
|
1572
|
+
errorType: null,
|
|
1573
|
+
latencyMs,
|
|
1574
|
+
inputTokens: null,
|
|
1575
|
+
outputTokens: null,
|
|
1576
|
+
transcriptChars: transcript.length,
|
|
1577
|
+
priorConsultations: prior.count,
|
|
1578
|
+
via: "opencode-session",
|
|
1579
|
+
gate: gateRecord,
|
|
1580
|
+
benchmarks: summarizeBenchmarkEvidence(benchmarkLoaded, null),
|
|
1581
|
+
});
|
|
1582
|
+
console.log(`[advisor] session=${sessionId} mode=${mode} trigger=${trigger} outcome=skipped_typesafe needed=${gateDecision.metrics.neededProbability ?? "n/a"} latencyMs=${latencyMs}`);
|
|
1583
|
+
const skipNote = gateSummary(gateRecord);
|
|
1584
|
+
return (`advisor consultation skipped (typesafe): the request did not need an advisor at this point` +
|
|
1585
|
+
`${gateDecision.metrics.neededProbability !== null ? ` (need probability ${gateDecision.metrics.neededProbability.toFixed(2)})` : ""}. ` +
|
|
1586
|
+
`Ask again with a concrete unresolved question if the situation changes.` +
|
|
1587
|
+
(skipNote ? `\n_${skipNote}_` : ""));
|
|
1588
|
+
}
|
|
1589
|
+
effectiveEffort =
|
|
1590
|
+
gateDecision?.status === "proceed" && gateDecision.effectiveEffort
|
|
1591
|
+
? gateDecision.effectiveEffort
|
|
1592
|
+
: requestedEffort;
|
|
1145
1593
|
const result = await callAdvisor({
|
|
1146
1594
|
runtime: opts.runtime,
|
|
1147
1595
|
systemPrompt,
|
|
1148
1596
|
transcript,
|
|
1149
1597
|
question: opts.question,
|
|
1150
1598
|
priorNote,
|
|
1151
|
-
effort:
|
|
1599
|
+
effort: effectiveEffort,
|
|
1152
1600
|
signal: opts.signal,
|
|
1153
1601
|
config,
|
|
1154
1602
|
});
|
|
1155
1603
|
const latencyMs = Date.now() - started;
|
|
1156
|
-
await logAdvisorMetrics({
|
|
1604
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1157
1605
|
ts: new Date().toISOString(),
|
|
1158
1606
|
sessionId,
|
|
1159
1607
|
callerModel,
|
|
@@ -1171,16 +1619,20 @@ async function runAdvisor(opts) {
|
|
|
1171
1619
|
transcriptChars: transcript.length,
|
|
1172
1620
|
priorConsultations: prior.count,
|
|
1173
1621
|
via: "opencode-session",
|
|
1622
|
+
gate: gateRecord,
|
|
1623
|
+
benchmarks: summarizeBenchmarkEvidence(benchmarkLoaded, result.variant ?? effectiveEffort ?? requestedEffort ?? null),
|
|
1174
1624
|
});
|
|
1175
1625
|
console.log(`[advisor] session=${sessionId} mode=${mode} trigger=${trigger} outcome=advisor_response latencyMs=${latencyMs}`);
|
|
1626
|
+
const gateNote = gateSummary(gateRecord);
|
|
1176
1627
|
return (result.text +
|
|
1177
|
-
`\n\n_advisor consultation #${prior.count + 1} in this session chain (trigger=${trigger})_`
|
|
1628
|
+
`\n\n_advisor consultation #${prior.count + 1} in this session chain (trigger=${trigger})_` +
|
|
1629
|
+
(gateNote ? `\n_${gateNote}_` : ""));
|
|
1178
1630
|
}
|
|
1179
1631
|
catch (err) {
|
|
1180
1632
|
const message = err instanceof Error ? err.message : String(err);
|
|
1181
1633
|
const errorType = classifyAdvisorError(message);
|
|
1182
1634
|
const latencyMs = Date.now() - started;
|
|
1183
|
-
await logAdvisorMetrics({
|
|
1635
|
+
await logAdvisorMetrics(opts.runtime, {
|
|
1184
1636
|
ts: new Date().toISOString(),
|
|
1185
1637
|
sessionId,
|
|
1186
1638
|
callerModel,
|
|
@@ -1189,7 +1641,7 @@ async function runAdvisor(opts) {
|
|
|
1189
1641
|
mode,
|
|
1190
1642
|
trigger,
|
|
1191
1643
|
questionChars,
|
|
1192
|
-
effort: requestedEffort ?? null,
|
|
1644
|
+
effort: effectiveEffort ?? requestedEffort ?? null,
|
|
1193
1645
|
outcome: "error",
|
|
1194
1646
|
errorType,
|
|
1195
1647
|
latencyMs,
|
|
@@ -1198,6 +1650,8 @@ async function runAdvisor(opts) {
|
|
|
1198
1650
|
transcriptChars: transcript.length,
|
|
1199
1651
|
priorConsultations: prior.count,
|
|
1200
1652
|
via: "opencode-session",
|
|
1653
|
+
gate: gateRecord,
|
|
1654
|
+
benchmarks: summarizeBenchmarkEvidence(benchmarkLoaded, effectiveEffort ?? requestedEffort ?? null),
|
|
1201
1655
|
});
|
|
1202
1656
|
console.log(`[advisor] session=${sessionId} mode=${mode} trigger=${trigger} outcome=error errorType=${errorType} latencyMs=${latencyMs}`);
|
|
1203
1657
|
throw new Error(`advisor failed (${errorType}): ${message}`);
|
|
@@ -1236,6 +1690,8 @@ export async function setupOcAdvisorV2(ctx) {
|
|
|
1236
1690
|
signal: context.abort,
|
|
1237
1691
|
callerAgent: context.agent,
|
|
1238
1692
|
callerDirectory: context.directory,
|
|
1693
|
+
callerMessageID: context.messageID,
|
|
1694
|
+
callerCallID: context.id,
|
|
1239
1695
|
config: advisorConfig,
|
|
1240
1696
|
});
|
|
1241
1697
|
return { content: text };
|
|
@@ -1251,15 +1707,17 @@ export async function setupOcAdvisorV2(ctx) {
|
|
|
1251
1707
|
return;
|
|
1252
1708
|
// `event.tools` lists the direct tools available to this request.
|
|
1253
1709
|
// 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
|
|
1710
|
+
// so the hook can only hide the tool (Fable or opted-out sessions),
|
|
1711
|
+
// never add it. The checkpoint instruction is injected only when
|
|
1712
|
+
// the tool is actually available, e.g. not when a permission rule
|
|
1713
|
+
// removed it.
|
|
1714
|
+
const disabled = advisorDisabledReason(event.model, advisorConfig);
|
|
1257
1715
|
let available = false;
|
|
1258
1716
|
for (const key of Object.keys(event.tools)) {
|
|
1259
1717
|
if (!isAdvisorToolName(key)) {
|
|
1260
1718
|
continue;
|
|
1261
1719
|
}
|
|
1262
|
-
if (
|
|
1720
|
+
if (disabled) {
|
|
1263
1721
|
delete event.tools[key];
|
|
1264
1722
|
}
|
|
1265
1723
|
else {
|
|
@@ -1292,5 +1750,5 @@ export const OcAdvisorPluginV2 = plugin;
|
|
|
1292
1750
|
export default plugin;
|
|
1293
1751
|
// Named exports for unit tests (bun test). The plugin entrypoint is the
|
|
1294
1752
|
// 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, };
|
|
1753
|
+
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
1754
|
//# sourceMappingURL=ocAdvisor.js.map
|