pi-advisor-flow 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -0
- package/dist/index.js +1816 -154
- package/package.json +11 -9
- package/src/commands/manual-consultation.ts +16 -2
- package/src/commands/settings-persistence.ts +48 -0
- package/src/commands/types.ts +1 -0
- package/src/config/defaults.ts +75 -0
- package/src/config/schema.ts +112 -0
- package/src/config/state.ts +69 -0
- package/src/config/storage.ts +7 -2
- package/src/config/types.ts +29 -0
- package/src/jev/client.ts +348 -0
- package/src/jev/key-store.ts +252 -0
- package/src/jev/ledger.ts +234 -0
- package/src/jev/questions.ts +119 -0
- package/src/jev/state.ts +47 -0
- package/src/jev/transport.ts +58 -0
- package/src/outcomes.ts +5 -1
- package/src/session-state.ts +99 -5
- package/src/tools/jev-filter.ts +202 -0
- package/src/tools/jev-turn-gate.ts +183 -0
- package/src/tools/loop-gate.ts +1 -0
- package/src/tools/outage-notifier.ts +30 -0
- package/src/tools/register-ask-advisor.ts +37 -1
- package/src/tools/register-renderers.ts +58 -0
- package/src/tools/registration.ts +17 -0
- package/src/tools/render-advisor-result.ts +43 -14
- package/src/tools/types.ts +10 -0
- package/src/ui/jev-setup-submenu.ts +344 -0
- package/src/ui/masked-input.ts +68 -0
- package/src/ui/settings-items.ts +162 -1
- package/src/ui/settings-mutations.ts +35 -0
- package/src/ui/types.ts +11 -0
- package/src/usage.ts +3 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { formatTokenCount } from "../usage.ts";
|
|
2
|
+
|
|
3
|
+
export interface AdvisorJevUsageTotals {
|
|
4
|
+
cost: number;
|
|
5
|
+
inputTokens: number;
|
|
6
|
+
outputTokens: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AdvisorJevFilterLedger {
|
|
10
|
+
allowed: number;
|
|
11
|
+
failures: number;
|
|
12
|
+
overrides: number;
|
|
13
|
+
repeatSkipped: number;
|
|
14
|
+
screened: number;
|
|
15
|
+
skipped: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AdvisorJevGateLedger {
|
|
19
|
+
checks: number;
|
|
20
|
+
consultations: number;
|
|
21
|
+
failures: number;
|
|
22
|
+
usage: AdvisorJevUsageTotals;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AdvisorJevLedger {
|
|
26
|
+
filter: AdvisorJevFilterLedger;
|
|
27
|
+
gate: AdvisorJevGateLedger;
|
|
28
|
+
usage: AdvisorJevUsageTotals;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AdvisorJevUsage {
|
|
32
|
+
cost: number;
|
|
33
|
+
inputTokens: number;
|
|
34
|
+
outputTokens: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface JevSkipRecord {
|
|
38
|
+
normalizedQuestion?: string;
|
|
39
|
+
turn: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Invocation fields the Jev summary reads; satisfied by AdvisorInvocationRecord. */
|
|
43
|
+
export interface JevInvocationView {
|
|
44
|
+
cost?: number;
|
|
45
|
+
kind: string;
|
|
46
|
+
trigger: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const freshJevUsage = (): AdvisorJevUsageTotals => ({
|
|
50
|
+
cost: 0,
|
|
51
|
+
inputTokens: 0,
|
|
52
|
+
outputTokens: 0,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const freshJevLedger = (): AdvisorJevLedger => ({
|
|
56
|
+
filter: {
|
|
57
|
+
allowed: 0,
|
|
58
|
+
failures: 0,
|
|
59
|
+
overrides: 0,
|
|
60
|
+
repeatSkipped: 0,
|
|
61
|
+
screened: 0,
|
|
62
|
+
skipped: 0,
|
|
63
|
+
},
|
|
64
|
+
gate: { checks: 0, consultations: 0, failures: 0, usage: freshJevUsage() },
|
|
65
|
+
usage: freshJevUsage(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const addJevUsage = (totals: AdvisorJevUsageTotals, usage: AdvisorJevUsage) => {
|
|
69
|
+
totals.cost += usage.cost;
|
|
70
|
+
totals.inputTokens += usage.inputTokens;
|
|
71
|
+
totals.outputTokens += usage.outputTokens;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** One session's Jev screening and turn-gate accounting plus summary lines. */
|
|
75
|
+
export class AdvisorJevLedgerState {
|
|
76
|
+
#ledger = freshJevLedger();
|
|
77
|
+
#lastSkip: JevSkipRecord | undefined;
|
|
78
|
+
|
|
79
|
+
reset() {
|
|
80
|
+
this.#ledger = freshJevLedger();
|
|
81
|
+
this.#lastSkip = undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get lastSkip() {
|
|
85
|
+
return this.#lastSkip;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
recordFilterAllowed() {
|
|
89
|
+
this.#ledger.filter.allowed += 1;
|
|
90
|
+
this.#ledger.filter.screened += 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
recordFilterSkipped(
|
|
94
|
+
repeat: boolean,
|
|
95
|
+
normalizedQuestion: string | undefined,
|
|
96
|
+
turn: number
|
|
97
|
+
) {
|
|
98
|
+
this.#ledger.filter.skipped += 1;
|
|
99
|
+
this.#ledger.filter.screened += 1;
|
|
100
|
+
if (repeat) {
|
|
101
|
+
this.#ledger.filter.repeatSkipped += 1;
|
|
102
|
+
}
|
|
103
|
+
this.#lastSkip = {
|
|
104
|
+
...(normalizedQuestion ? { normalizedQuestion } : {}),
|
|
105
|
+
turn,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
recordFilterOverride() {
|
|
110
|
+
this.#ledger.filter.overrides += 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
recordFilterFailure() {
|
|
114
|
+
this.#ledger.filter.failures += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
recordFilterUsage(usage: AdvisorJevUsage) {
|
|
118
|
+
addJevUsage(this.#ledger.usage, usage);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
recordGateCheck(usage?: AdvisorJevUsage) {
|
|
122
|
+
this.#ledger.gate.checks += 1;
|
|
123
|
+
if (usage) {
|
|
124
|
+
addJevUsage(this.#ledger.gate.usage, usage);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
recordGateConsultation() {
|
|
129
|
+
this.#ledger.gate.consultations += 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
recordGateFailure() {
|
|
133
|
+
this.#ledger.gate.failures += 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
summaryLines(invocations: readonly JevInvocationView[]): string[] {
|
|
137
|
+
const lines: string[] = [];
|
|
138
|
+
const { filter, gate, usage } = this.#ledger;
|
|
139
|
+
const nonRepeatJevActivity =
|
|
140
|
+
filter.allowed +
|
|
141
|
+
(filter.skipped - filter.repeatSkipped) +
|
|
142
|
+
filter.failures;
|
|
143
|
+
if (nonRepeatJevActivity === 0 && filter.repeatSkipped > 0) {
|
|
144
|
+
// Only dedup fired — no Jev call ever happened, so the line must not
|
|
145
|
+
// claim Jev activity.
|
|
146
|
+
const parts = [
|
|
147
|
+
`${filter.repeatSkipped} repeat question${filter.repeatSkipped === 1 ? "" : "s"} skipped, earlier advice reattached`,
|
|
148
|
+
];
|
|
149
|
+
if (filter.overrides > 0) {
|
|
150
|
+
parts.push(
|
|
151
|
+
`${filter.overrides} override${filter.overrides === 1 ? "" : "s"}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
lines.push(`Consultation dedup: ${parts.join(", ")}`);
|
|
155
|
+
lines.push(
|
|
156
|
+
this.#savingsLine(this.#markdownCosts(invocations), filter.skipped)
|
|
157
|
+
);
|
|
158
|
+
} else if (this.#filterActive()) {
|
|
159
|
+
lines.push(this.#filterLine(filter));
|
|
160
|
+
const jevTokens = usage.inputTokens + usage.outputTokens;
|
|
161
|
+
if (jevTokens > 0) {
|
|
162
|
+
lines.push(
|
|
163
|
+
`Jev cost: ${this.#formatJevTokens(usage)} tokens · $${usage.cost.toFixed(4)} (input only; output free)`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (filter.skipped > 0) {
|
|
167
|
+
lines.push(
|
|
168
|
+
this.#savingsLine(this.#markdownCosts(invocations), filter.skipped)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (gate.checks > 0 || gate.consultations > 0) {
|
|
173
|
+
lines.push(this.#gateLine(gate, invocations));
|
|
174
|
+
}
|
|
175
|
+
return lines;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#filterActive() {
|
|
179
|
+
const { filter } = this.#ledger;
|
|
180
|
+
return filter.screened > 0 || filter.overrides > 0 || filter.failures > 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#savingsLine(markdownCosts: number[], skipped: number) {
|
|
184
|
+
if (markdownCosts.length === 0) {
|
|
185
|
+
return "Estimated saving from skips: unavailable — no observed consultation cost this session";
|
|
186
|
+
}
|
|
187
|
+
const mean =
|
|
188
|
+
markdownCosts.reduce((sum, cost) => sum + cost, 0) / markdownCosts.length;
|
|
189
|
+
return `Estimated saving from skips: ≤ $${(mean * skipped).toFixed(4)} — upper bound; assumes each skipped consultation would have cost this session's mean allowed-consultation cost ($${mean.toFixed(4)}), which the skipped calls would likely have undercut`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#gateLine(
|
|
193
|
+
gate: AdvisorJevGateLedger,
|
|
194
|
+
invocations: readonly JevInvocationView[]
|
|
195
|
+
) {
|
|
196
|
+
const consultationCosts = invocations
|
|
197
|
+
.filter(
|
|
198
|
+
(item): item is JevInvocationView & { cost: number } =>
|
|
199
|
+
item.trigger === "turn-gate" && typeof item.cost === "number"
|
|
200
|
+
)
|
|
201
|
+
.map((item) => item.cost);
|
|
202
|
+
const gateSpend = consultationCosts.reduce((sum, cost) => sum + cost, 0);
|
|
203
|
+
return `Turn gate: ${gate.checks} check${gate.checks === 1 ? "" : "s"} (Jev ${this.#formatJevTokens(gate.usage)} · $${gate.usage.cost.toFixed(4)}), ${gate.consultations} consultation${gate.consultations === 1 ? "" : "s"} ($${gateSpend.toFixed(4)})`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#formatJevTokens(usage: AdvisorJevUsageTotals) {
|
|
207
|
+
return `↑${formatTokenCount(usage.inputTokens + usage.outputTokens)}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#markdownCosts(invocations: readonly JevInvocationView[]): number[] {
|
|
211
|
+
return invocations
|
|
212
|
+
.filter(
|
|
213
|
+
(item): item is JevInvocationView & { cost: number } =>
|
|
214
|
+
item.kind === "markdown" && typeof item.cost === "number"
|
|
215
|
+
)
|
|
216
|
+
.map((item) => item.cost);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#filterLine(filter: AdvisorJevFilterLedger) {
|
|
220
|
+
const head = `${filter.screened} screened (${filter.allowed} allowed, ${filter.skipped} skipped${filter.repeatSkipped > 0 ? ` [${filter.repeatSkipped} repeat]` : ""})`;
|
|
221
|
+
const parts = [head];
|
|
222
|
+
if (filter.overrides > 0) {
|
|
223
|
+
parts.push(
|
|
224
|
+
`${filter.overrides} override${filter.overrides === 1 ? "" : "s"}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (filter.failures > 0) {
|
|
228
|
+
parts.push(
|
|
229
|
+
`${filter.failures} failure${filter.failures === 1 ? "" : "s"}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return `Jev filter: ${parts.join(", ")}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Questions } from "@typesafe-ai/sdk";
|
|
2
|
+
|
|
3
|
+
/** Exact rubric text for the stakes Score question; level 0 is the skip level. */
|
|
4
|
+
export const STAKES_RUBRIC = [
|
|
5
|
+
"Negligible: routine, low-risk, mechanical, or reversible; a wrong call costs little and is easy to undo.",
|
|
6
|
+
"Moderate: some risk or rework, but bounded and recoverable.",
|
|
7
|
+
"High: material consequences for correctness, security, cost, user trust, or irreversibility.",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
const EVIDENCE_RULE =
|
|
11
|
+
"Judge from `executor_question` and `executor_draft` when present, otherwise from `recent_conversation`; when both are absent, `recent_conversation` is the evidence to judge from.";
|
|
12
|
+
|
|
13
|
+
export const screeningQuestions: Questions = {
|
|
14
|
+
self_answerable: {
|
|
15
|
+
criteria: {
|
|
16
|
+
false: "The executor needs the Advisor's second opinion.",
|
|
17
|
+
true: "The executor can resolve this alone with available tools and context.",
|
|
18
|
+
},
|
|
19
|
+
instructions: `Can the executor confidently resolve this request alone, using available tools and context? ${EVIDENCE_RULE}`,
|
|
20
|
+
type: "noul",
|
|
21
|
+
},
|
|
22
|
+
stakes: {
|
|
23
|
+
criteria: [...STAKES_RUBRIC],
|
|
24
|
+
instructions: `How material are the stakes of the decision behind this consultation request? ${EVIDENCE_RULE}`,
|
|
25
|
+
type: "score",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface TurnGateQuestions {
|
|
30
|
+
instructions: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ScreeningCriteria {
|
|
34
|
+
noulMargin: number;
|
|
35
|
+
skipConfidence: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ScreeningVerdict = { skip: false } | { skip: true };
|
|
39
|
+
|
|
40
|
+
const NUMERIC_KEY_PATTERN = /^\d+$/;
|
|
41
|
+
|
|
42
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
43
|
+
Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
44
|
+
|
|
45
|
+
const finiteNumber = (value: unknown): number | undefined =>
|
|
46
|
+
typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Probability mass on the lowest stakes level, resolved through the answer's
|
|
50
|
+
* legend (the key whose description equals level 0), falling back to the
|
|
51
|
+
* smallest numeric index key only when no legend resolves. Shape-independent:
|
|
52
|
+
* never hardcodes a level key and never uses `ceil(score)`.
|
|
53
|
+
*/
|
|
54
|
+
export const lowestStakesProbability = (
|
|
55
|
+
answer: unknown
|
|
56
|
+
): number | undefined => {
|
|
57
|
+
if (!isRecord(answer)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const probabilities = isRecord(answer.probabilities)
|
|
61
|
+
? answer.probabilities
|
|
62
|
+
: {};
|
|
63
|
+
const legend = isRecord(answer.legend) ? answer.legend : undefined;
|
|
64
|
+
if (legend) {
|
|
65
|
+
const exact = Object.keys(legend).find(
|
|
66
|
+
(key) => legend[key] === STAKES_RUBRIC[0]
|
|
67
|
+
);
|
|
68
|
+
if (exact) {
|
|
69
|
+
return finiteNumber(probabilities[exact]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const numericKeys = Object.keys(probabilities).filter((key) =>
|
|
73
|
+
NUMERIC_KEY_PATTERN.test(key)
|
|
74
|
+
);
|
|
75
|
+
if (numericKeys.length === 0) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const lowest = numericKeys.reduce((left, right) =>
|
|
79
|
+
Number(left) <= Number(right) ? left : right
|
|
80
|
+
);
|
|
81
|
+
return finiteNumber(probabilities[lowest]);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const selfAnswerableNoul = (answer: unknown): number | undefined =>
|
|
85
|
+
isRecord(answer) ? finiteNumber(answer.noul) : undefined;
|
|
86
|
+
|
|
87
|
+
/** Pure composition: skip requires the hard conjunction of confident
|
|
88
|
+
* negligible stakes AND confident self-answerability. Any missing, NaN, or
|
|
89
|
+
* malformed input allows. Never a weighted sum. */
|
|
90
|
+
export const composeScreeningVerdict = (
|
|
91
|
+
answers: unknown,
|
|
92
|
+
{ noulMargin, skipConfidence }: ScreeningCriteria
|
|
93
|
+
): ScreeningVerdict => {
|
|
94
|
+
if (!isRecord(answers)) {
|
|
95
|
+
return { skip: false };
|
|
96
|
+
}
|
|
97
|
+
const negligibleMass = lowestStakesProbability(answers.stakes);
|
|
98
|
+
const noul = selfAnswerableNoul(answers.self_answerable);
|
|
99
|
+
if (negligibleMass === undefined || noul === undefined) {
|
|
100
|
+
return { skip: false };
|
|
101
|
+
}
|
|
102
|
+
const confidentlySelfAnswerable = noul >= 0.5 + noulMargin;
|
|
103
|
+
return {
|
|
104
|
+
skip: negligibleMass >= skipConfidence && confidentlySelfAnswerable,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Confident-true only; any uncertainty means no invocation. */
|
|
109
|
+
export const composeTurnGateVerdict = (
|
|
110
|
+
answers: unknown,
|
|
111
|
+
threshold: number
|
|
112
|
+
): boolean => {
|
|
113
|
+
if (!isRecord(answers)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const answer = answers.should_consult;
|
|
117
|
+
const noul = isRecord(answer) ? finiteNumber(answer.noul) : undefined;
|
|
118
|
+
return noul !== undefined && noul >= threshold;
|
|
119
|
+
};
|
package/src/jev/state.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { JsonValue } from "@typesafe-ai/sdk";
|
|
3
|
+
import {
|
|
4
|
+
advisorJevDigestMaxCharsRef,
|
|
5
|
+
advisorRedactSecretsRef,
|
|
6
|
+
} from "../config/state.ts";
|
|
7
|
+
import { recentConversation } from "../conversation.ts";
|
|
8
|
+
import { redactAndCapText } from "../redaction.ts";
|
|
9
|
+
|
|
10
|
+
/** Per-field byte cap for executor question and draft sent to Jev. */
|
|
11
|
+
export const JEV_TEXT_CAP_BYTES = 8 * 1024;
|
|
12
|
+
|
|
13
|
+
export interface JevStateInput {
|
|
14
|
+
draft?: string;
|
|
15
|
+
question?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type JevState = Record<string, JsonValue>;
|
|
19
|
+
|
|
20
|
+
/** Builds the Jev screening state: named fields, redacted and capped through
|
|
21
|
+
* the existing egress pipeline, with the conversation digest as fallback
|
|
22
|
+
* evidence when no explicit request exists. */
|
|
23
|
+
export const buildJevState = (
|
|
24
|
+
ctx: ExtensionContext,
|
|
25
|
+
input: JevStateInput = {}
|
|
26
|
+
): JevState => {
|
|
27
|
+
const state: JevState = { role: "executor" };
|
|
28
|
+
if (input.question) {
|
|
29
|
+
state.executor_question = redactAndCapText(
|
|
30
|
+
input.question,
|
|
31
|
+
JEV_TEXT_CAP_BYTES,
|
|
32
|
+
advisorRedactSecretsRef
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (input.draft) {
|
|
36
|
+
state.executor_draft = redactAndCapText(
|
|
37
|
+
input.draft,
|
|
38
|
+
JEV_TEXT_CAP_BYTES,
|
|
39
|
+
advisorRedactSecretsRef
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const digest = recentConversation(ctx, advisorJevDigestMaxCharsRef);
|
|
43
|
+
if (digest) {
|
|
44
|
+
state.recent_conversation = digest;
|
|
45
|
+
}
|
|
46
|
+
return state;
|
|
47
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { advisorJevTransportRef } from "../config/state.ts";
|
|
3
|
+
import { type JevKeySource, resolveTypeSafeKey } from "./key-store.ts";
|
|
4
|
+
|
|
5
|
+
export type JevTransportKind = "typesafe" | "openrouter";
|
|
6
|
+
|
|
7
|
+
export interface JevCredentials {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
/** Where the TypeSafe key came from; present on the typesafe transport. */
|
|
10
|
+
source?: JevKeySource;
|
|
11
|
+
transport: JevTransportKind;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JevTransportDeps {
|
|
15
|
+
/** Reads a pi-stored provider login; injectable for tests. */
|
|
16
|
+
getProviderKey?: (provider: string) => Promise<string | undefined>;
|
|
17
|
+
/** Resolves the TypeSafe key chain; injectable for tests. */
|
|
18
|
+
resolveTypesafe?: () => ReturnType<typeof resolveTypeSafeKey>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const OPENROUTER_PROVIDER = "openrouter";
|
|
22
|
+
|
|
23
|
+
const openRouterKey = async (
|
|
24
|
+
ctx: ExtensionContext | undefined,
|
|
25
|
+
deps: JevTransportDeps
|
|
26
|
+
): Promise<string | undefined> => {
|
|
27
|
+
const key = deps.getProviderKey
|
|
28
|
+
? await deps.getProviderKey(OPENROUTER_PROVIDER)
|
|
29
|
+
: await ctx?.modelRegistry?.getApiKeyForProvider(OPENROUTER_PROVIDER);
|
|
30
|
+
return key?.trim() || undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Resolves how Jev calls authenticate: a dedicated TypeSafe key first, then
|
|
34
|
+
* the existing pi OpenRouter login (reuse per advisorJevTransport). */
|
|
35
|
+
export const resolveJevTransport = async (
|
|
36
|
+
ctx?: ExtensionContext,
|
|
37
|
+
deps: JevTransportDeps = {}
|
|
38
|
+
): Promise<JevCredentials | undefined> => {
|
|
39
|
+
const preference = advisorJevTransportRef;
|
|
40
|
+
if (preference !== "openrouter") {
|
|
41
|
+
const resolveTypesafe = deps.resolveTypesafe ?? resolveTypeSafeKey;
|
|
42
|
+
const resolution = await resolveTypesafe();
|
|
43
|
+
if (resolution.key) {
|
|
44
|
+
return {
|
|
45
|
+
apiKey: resolution.key,
|
|
46
|
+
...(resolution.source ? { source: resolution.source } : {}),
|
|
47
|
+
transport: "typesafe",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (preference === "typesafe") {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const openrouter = await openRouterKey(ctx, deps);
|
|
55
|
+
return openrouter
|
|
56
|
+
? { apiKey: openrouter, transport: "openrouter" }
|
|
57
|
+
: undefined;
|
|
58
|
+
};
|
package/src/outcomes.ts
CHANGED
|
@@ -15,7 +15,11 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
15
15
|
|
|
16
16
|
export type OutcomeAdoption = "followed" | "not-followed" | "unknown";
|
|
17
17
|
export type OutcomeValidation = "passed" | "failed" | "not-run" | "unknown";
|
|
18
|
-
type OutcomeTrigger =
|
|
18
|
+
type OutcomeTrigger =
|
|
19
|
+
| "manual"
|
|
20
|
+
| "executor-requested"
|
|
21
|
+
| "turn-gate"
|
|
22
|
+
| "repeated-tool-call";
|
|
19
23
|
export const ADOPTIONS: OutcomeAdoption[] = [
|
|
20
24
|
"followed",
|
|
21
25
|
"not-followed",
|
package/src/session-state.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AdvisorJevLedgerState, type AdvisorJevUsage } from "./jev/ledger.ts";
|
|
1
2
|
import {
|
|
2
3
|
type AdvisorUsageTotals,
|
|
3
4
|
addAdvisorUsage,
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
} from "./usage.ts";
|
|
8
9
|
|
|
9
10
|
export type GateDecision = "proceed" | "revise" | "blocked";
|
|
10
|
-
export type ConsultationTrigger = "manual" | "executor-requested";
|
|
11
|
+
export type ConsultationTrigger = "manual" | "executor-requested" | "turn-gate";
|
|
11
12
|
export type GateTrigger =
|
|
12
13
|
| "repeated-tool-call"
|
|
13
14
|
| "completion-review"
|
|
@@ -121,7 +122,14 @@ interface RepetitionState {
|
|
|
121
122
|
|
|
122
123
|
interface AdviceLedger {
|
|
123
124
|
draftConsultations: number;
|
|
124
|
-
issued: Map<
|
|
125
|
+
issued: Map<
|
|
126
|
+
string,
|
|
127
|
+
{
|
|
128
|
+
advice: string;
|
|
129
|
+
normalizedQuestion?: string;
|
|
130
|
+
trigger: ConsultationTrigger;
|
|
131
|
+
}
|
|
132
|
+
>;
|
|
125
133
|
lastAdvice?: string;
|
|
126
134
|
outcomes: number;
|
|
127
135
|
pending: Set<string>;
|
|
@@ -155,6 +163,9 @@ export class AdvisorSessionState {
|
|
|
155
163
|
#ledger = freshAdviceLedger();
|
|
156
164
|
#usage = freshUsage();
|
|
157
165
|
#consumedCalls = 0;
|
|
166
|
+
readonly #jev = new AdvisorJevLedgerState();
|
|
167
|
+
#sessionTurnOrdinal = 0;
|
|
168
|
+
#turnsSinceConsultation = 0;
|
|
158
169
|
|
|
159
170
|
resetTask() {
|
|
160
171
|
this.#repetition = freshRepetition();
|
|
@@ -162,6 +173,9 @@ export class AdvisorSessionState {
|
|
|
162
173
|
this.#ledger = freshAdviceLedger();
|
|
163
174
|
this.#usage = freshUsage();
|
|
164
175
|
this.#consumedCalls = 0;
|
|
176
|
+
this.#jev.reset();
|
|
177
|
+
this.#sessionTurnOrdinal = 0;
|
|
178
|
+
this.#turnsSinceConsultation = 0;
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
clearBlocked() {
|
|
@@ -214,6 +228,26 @@ export class AdvisorSessionState {
|
|
|
214
228
|
return this.#consumedCalls;
|
|
215
229
|
}
|
|
216
230
|
|
|
231
|
+
/** Counts one completed executor turn on both turn counters. The ordinal
|
|
232
|
+
* feeds the filter override window and summary positions and is never reset
|
|
233
|
+
* by anything; turnsSinceConsultation resets on every consultation. */
|
|
234
|
+
recordCompletedTurn() {
|
|
235
|
+
this.#sessionTurnOrdinal += 1;
|
|
236
|
+
this.#turnsSinceConsultation += 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
resetTurnsSinceConsultation() {
|
|
240
|
+
this.#turnsSinceConsultation = 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get sessionTurnOrdinal() {
|
|
244
|
+
return this.#sessionTurnOrdinal;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
get turnsSinceConsultation() {
|
|
248
|
+
return this.#turnsSinceConsultation;
|
|
249
|
+
}
|
|
250
|
+
|
|
217
251
|
/** Returns a copy of cumulative direct Advisor usage for this session. */
|
|
218
252
|
get usageTotals(): AdvisorUsageTotals {
|
|
219
253
|
return { ...this.#usage.totals };
|
|
@@ -232,14 +266,34 @@ export class AdvisorSessionState {
|
|
|
232
266
|
id: string,
|
|
233
267
|
advice: string,
|
|
234
268
|
trigger: ConsultationTrigger,
|
|
235
|
-
draft = false
|
|
269
|
+
draft = false,
|
|
270
|
+
normalizedQuestion?: string
|
|
236
271
|
) {
|
|
237
|
-
this.#ledger.issued.set(id, {
|
|
272
|
+
this.#ledger.issued.set(id, {
|
|
273
|
+
advice,
|
|
274
|
+
...(normalizedQuestion ? { normalizedQuestion } : {}),
|
|
275
|
+
trigger,
|
|
276
|
+
});
|
|
238
277
|
this.#ledger.lastAdvice = advice;
|
|
239
278
|
if (draft) {
|
|
240
279
|
this.#ledger.draftConsultations += 1;
|
|
241
280
|
}
|
|
242
281
|
}
|
|
282
|
+
|
|
283
|
+
/** Returns earlier advice for an exactly-matching normalized question. */
|
|
284
|
+
reattachedAdviceFor(
|
|
285
|
+
normalizedQuestion: string | undefined
|
|
286
|
+
): string | undefined {
|
|
287
|
+
if (!normalizedQuestion) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
for (const entry of this.#ledger.issued.values()) {
|
|
291
|
+
if (entry.normalizedQuestion === normalizedQuestion) {
|
|
292
|
+
return entry.advice;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
243
297
|
claimTrackedFiles(paths: string[]) {
|
|
244
298
|
const advice = this.#ledger.lastAdvice;
|
|
245
299
|
if (!advice || paths.length === 0) {
|
|
@@ -324,6 +378,7 @@ export class AdvisorSessionState {
|
|
|
324
378
|
[
|
|
325
379
|
"manual",
|
|
326
380
|
"executor-requested",
|
|
381
|
+
"turn-gate",
|
|
327
382
|
"repeated-tool-call",
|
|
328
383
|
"completion-review",
|
|
329
384
|
"custom-rule",
|
|
@@ -333,9 +388,47 @@ export class AdvisorSessionState {
|
|
|
333
388
|
);
|
|
334
389
|
}
|
|
335
390
|
|
|
391
|
+
recordJevFilterAllowed() {
|
|
392
|
+
this.#jev.recordFilterAllowed();
|
|
393
|
+
}
|
|
394
|
+
recordJevFilterSkipped(repeat: boolean, normalizedQuestion?: string) {
|
|
395
|
+
this.#jev.recordFilterSkipped(
|
|
396
|
+
repeat,
|
|
397
|
+
normalizedQuestion,
|
|
398
|
+
this.#sessionTurnOrdinal
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
get lastJevSkip() {
|
|
402
|
+
return this.#jev.lastSkip;
|
|
403
|
+
}
|
|
404
|
+
recordJevFilterOverride() {
|
|
405
|
+
this.#jev.recordFilterOverride();
|
|
406
|
+
}
|
|
407
|
+
recordJevFilterFailure() {
|
|
408
|
+
this.#jev.recordFilterFailure();
|
|
409
|
+
}
|
|
410
|
+
recordJevFilterUsage(usage: AdvisorJevUsage) {
|
|
411
|
+
this.#jev.recordFilterUsage(usage);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
recordJevGateCheck(usage?: AdvisorJevUsage) {
|
|
415
|
+
this.#jev.recordGateCheck(usage);
|
|
416
|
+
}
|
|
417
|
+
recordJevGateConsultation() {
|
|
418
|
+
this.#jev.recordGateConsultation();
|
|
419
|
+
}
|
|
420
|
+
recordJevGateFailure() {
|
|
421
|
+
this.#jev.recordGateFailure();
|
|
422
|
+
}
|
|
423
|
+
|
|
336
424
|
summary(limit: number | undefined) {
|
|
337
425
|
const { invocations, totals } = this.#usage;
|
|
338
|
-
|
|
426
|
+
const jevLines = this.#jev.summaryLines(invocations);
|
|
427
|
+
if (
|
|
428
|
+
invocations.length === 0 &&
|
|
429
|
+
this.#repetition.interventions === 0 &&
|
|
430
|
+
jevLines.length === 0
|
|
431
|
+
) {
|
|
339
432
|
return;
|
|
340
433
|
}
|
|
341
434
|
const markdown = invocations.filter((item) => item.kind === "markdown");
|
|
@@ -366,6 +459,7 @@ export class AdvisorSessionState {
|
|
|
366
459
|
`Loop matching: normalized tool signatures; ${this.#repetition.interventions} gate intervention${this.#repetition.interventions === 1 ? "" : "s"}`,
|
|
367
460
|
`Execution effects: ${effects("tool-blocked")} tool blocked, ${effects("session-blocked")} sessions blocked, ${effects("continued")} continued`,
|
|
368
461
|
`Failures: ${failures.length ? failures.join(", ") : "none"}`,
|
|
462
|
+
...jevLines,
|
|
369
463
|
].join("\n");
|
|
370
464
|
}
|
|
371
465
|
}
|