pi-openai-toolkit 0.8.0 → 0.10.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/README.md +84 -13
- package/README_zh.md +84 -13
- package/extensions/auto-mode.ts +1 -0
- package/package.json +13 -3
- package/src/auto-mode/classifier.ts +165 -0
- package/src/auto-mode/extension.ts +530 -0
- package/src/auto-mode/policy.ts +146 -0
- package/src/auto-mode/prompt.ts +279 -0
- package/src/auto-mode/reviewer.ts +325 -0
- package/src/auto-mode/transcript.ts +133 -0
- package/src/auto-mode/types.ts +137 -0
- package/src/config.ts +255 -16
- package/src/extension-runtime.ts +12 -4
- package/src/image-generation/eligibility.ts +7 -7
- package/src/image-generation/extension.ts +160 -160
- package/src/native-fallback.ts +19 -6
- package/src/types.ts +107 -10
- package/tsconfig.check.json +5 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
boundReviewText,
|
|
4
|
+
MAX_TRANSCRIPT_CHARS,
|
|
5
|
+
MAX_TRANSCRIPT_ENTRY_CHARS,
|
|
6
|
+
MAX_TRANSCRIPT_RECENT_ENTRIES,
|
|
7
|
+
MAX_TRANSCRIPT_TOOL_CHARS,
|
|
8
|
+
TRUNCATION_MARKER,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A transcript line already reduced to text, with the role kept so the reviewer can
|
|
13
|
+
* tell whose words these are. `[user]` is the only role that carries authorization.
|
|
14
|
+
*/
|
|
15
|
+
export type TranscriptLine = {
|
|
16
|
+
role: "user" | "assistant" | "tool";
|
|
17
|
+
text: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function textOfContent(content: unknown): string {
|
|
21
|
+
if (typeof content === "string") return content;
|
|
22
|
+
if (!Array.isArray(content)) return "";
|
|
23
|
+
return content
|
|
24
|
+
.map((block) => {
|
|
25
|
+
if (!block || typeof block !== "object") return "";
|
|
26
|
+
const record = block as Record<string, unknown>;
|
|
27
|
+
if (record.type === "text" && typeof record.text === "string") return record.text;
|
|
28
|
+
if (record.type === "image") return "[image omitted]";
|
|
29
|
+
if (record.type === "toolCall" && typeof record.name === "string") {
|
|
30
|
+
const args = record.arguments;
|
|
31
|
+
return `${record.name} ${typeof args === "object" && args ? JSON.stringify(args) : ""}`.trim();
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
})
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join("\n")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Flatten session entries into reviewer lines. Non-message entries are skipped,
|
|
42
|
+
* except a compaction summary, which is kept as a `[user]`-adjacent context marker
|
|
43
|
+
* because it is the only surviving view of the older conversation.
|
|
44
|
+
*/
|
|
45
|
+
export function sessionEntriesToLines(entries: readonly SessionEntry[]): TranscriptLine[] {
|
|
46
|
+
const lines: TranscriptLine[] = [];
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
if (entry.type === "compaction" && typeof entry.summary === "string") {
|
|
49
|
+
const summary = boundReviewText(entry.summary, MAX_TRANSCRIPT_ENTRY_CHARS);
|
|
50
|
+
if (summary) lines.push({ role: "assistant", text: `[earlier conversation summary] ${summary}` });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (entry.type !== "message") continue;
|
|
54
|
+
const message = entry.message as { role?: unknown; content?: unknown };
|
|
55
|
+
const text = textOfContent(message.content);
|
|
56
|
+
if (!text) continue;
|
|
57
|
+
if (message.role === "user") lines.push({ role: "user", text });
|
|
58
|
+
else if (message.role === "assistant") lines.push({ role: "assistant", text });
|
|
59
|
+
else if (message.role === "toolResult") lines.push({ role: "tool", text });
|
|
60
|
+
}
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderLine(index: number, line: TranscriptLine): string {
|
|
65
|
+
const body = boundReviewText(line.text, MAX_TRANSCRIPT_ENTRY_CHARS);
|
|
66
|
+
return `[${index}] [${line.role}]: ${body}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a bounded transcript the way Codex's guardian does: user turns are the
|
|
71
|
+
* anchors and are protected, tool and assistant evidence gets its own smaller
|
|
72
|
+
* budget so verbose command output cannot crowd out the human conversation that
|
|
73
|
+
* actually establishes authorization.
|
|
74
|
+
*
|
|
75
|
+
* Selection order is newest-first, then rendered in chronological order.
|
|
76
|
+
*/
|
|
77
|
+
export function buildReviewTranscript(
|
|
78
|
+
lines: readonly TranscriptLine[],
|
|
79
|
+
budgets: {
|
|
80
|
+
maxTotalChars?: number;
|
|
81
|
+
maxToolChars?: number;
|
|
82
|
+
maxRecentEntries?: number;
|
|
83
|
+
} = {},
|
|
84
|
+
): { text: string; omitted: boolean } {
|
|
85
|
+
const maxTotalChars = budgets.maxTotalChars ?? MAX_TRANSCRIPT_CHARS;
|
|
86
|
+
const maxToolChars = budgets.maxToolChars ?? MAX_TRANSCRIPT_TOOL_CHARS;
|
|
87
|
+
const maxRecentEntries = budgets.maxRecentEntries ?? MAX_TRANSCRIPT_RECENT_ENTRIES;
|
|
88
|
+
|
|
89
|
+
const recent = lines.slice(-Math.max(1, maxRecentEntries));
|
|
90
|
+
const omitted = recent.length < lines.length;
|
|
91
|
+
|
|
92
|
+
const userLines: number[] = [];
|
|
93
|
+
const otherLines: number[] = [];
|
|
94
|
+
recent.forEach((line, index) => {
|
|
95
|
+
if (line.role === "user") userLines.push(index);
|
|
96
|
+
else otherLines.push(index);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const selected = new Set<number>();
|
|
100
|
+
let userChars = 0;
|
|
101
|
+
// The first user turn carries the task; the latest carries the immediate ask.
|
|
102
|
+
for (const index of [userLines[0]!, userLines[userLines.length - 1]!, ...userLines.slice(1, -1).reverse()]) {
|
|
103
|
+
if (index === undefined || selected.has(index)) continue;
|
|
104
|
+
const cost = recent[index]!.text.length;
|
|
105
|
+
if (userChars + cost > maxTotalChars) break;
|
|
106
|
+
selected.add(index);
|
|
107
|
+
userChars += cost;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let toolChars = 0;
|
|
111
|
+
for (const index of [...otherLines].reverse()) {
|
|
112
|
+
const cost = recent[index]!.text.length;
|
|
113
|
+
if (toolChars + cost > maxToolChars) continue;
|
|
114
|
+
if (userChars + toolChars + cost > maxTotalChars) continue;
|
|
115
|
+
selected.add(index);
|
|
116
|
+
toolChars += cost;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ordered = [...selected].sort((a, b) => a - b);
|
|
120
|
+
const rendered = ordered.map((index) => renderLine(index + 1, recent[index]!));
|
|
121
|
+
if (omitted) rendered.unshift(`${TRUNCATION_MARKER} earlier conversation entries were omitted.`);
|
|
122
|
+
return { text: rendered.join("\n"), omitted };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build the reviewer transcript straight from session entries.
|
|
127
|
+
*/
|
|
128
|
+
export function transcriptFromEntries(
|
|
129
|
+
entries: readonly SessionEntry[],
|
|
130
|
+
budgets?: Parameters<typeof buildReviewTranscript>[1],
|
|
131
|
+
): { text: string; omitted: boolean } {
|
|
132
|
+
return buildReviewTranscript(sessionEntriesToLines(entries), budgets);
|
|
133
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { AutoModeConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export const AUTO_MODE_ENTRY_TYPE = "pi-openai-toolkit.auto-mode-decision.v1";
|
|
4
|
+
export const AUTO_MODE_STATUS_KEY = "pi-openai-toolkit:auto-mode";
|
|
5
|
+
export const AUTO_MODE_FLAG = "auto";
|
|
6
|
+
export const AUTO_MODE_COMMAND = "auto";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Built-in tools that change state outside the conversation. Everything else is
|
|
10
|
+
* read-only by default and only reviewed when the user opts in through
|
|
11
|
+
* `autoMode.extraTools` or `gate: "all"`.
|
|
12
|
+
*/
|
|
13
|
+
export const SIDE_EFFECT_TOOLS = ["bash", "write", "edit"] as const;
|
|
14
|
+
|
|
15
|
+
export const MAX_REVIEW_INPUT_CHARS = 8_000;
|
|
16
|
+
export const MAX_REVIEW_INTENT_CHARS = 2_000;
|
|
17
|
+
export const MAX_REVIEW_REASON_CHARS = 600;
|
|
18
|
+
export const MAX_REVIEW_MODEL_KEY_CHARS = 256;
|
|
19
|
+
|
|
20
|
+
/** Budgets for the compact transcript handed to the reviewer. */
|
|
21
|
+
export const MAX_TRANSCRIPT_CHARS = 24_000;
|
|
22
|
+
export const MAX_TRANSCRIPT_TOOL_CHARS = 12_000;
|
|
23
|
+
export const MAX_TRANSCRIPT_ENTRY_CHARS = 2_000;
|
|
24
|
+
export const MAX_TRANSCRIPT_RECENT_ENTRIES = 40;
|
|
25
|
+
export const TRUNCATION_MARKER = "<truncated />";
|
|
26
|
+
|
|
27
|
+
/** Risk of the planned action itself, as scored by the reviewer. */
|
|
28
|
+
export type RiskLevel = "low" | "medium" | "high" | "critical";
|
|
29
|
+
|
|
30
|
+
/** How far the observed conversation actually authorizes that action. */
|
|
31
|
+
export type UserAuthorization = "unknown" | "low" | "medium" | "high";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Why a review could not be completed. Kept separate from `deny` on purpose: an
|
|
35
|
+
* infrastructure failure must never be reported to the agent as a safety verdict.
|
|
36
|
+
*/
|
|
37
|
+
export type ReviewFailureCause =
|
|
38
|
+
| "not-configured"
|
|
39
|
+
| "timeout"
|
|
40
|
+
| "cancelled"
|
|
41
|
+
| "provider-error"
|
|
42
|
+
| "invalid-output";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The reviewer's full verdict. Only `outcome` is required from the model; the
|
|
46
|
+
* remaining fields are back-filled by `parseReviewVerdict` so that a terse
|
|
47
|
+
* `{"outcome":"allow"}` answer is still a usable, recordable decision.
|
|
48
|
+
*/
|
|
49
|
+
export type GuardianVerdict = {
|
|
50
|
+
outcome: "allow" | "deny";
|
|
51
|
+
riskLevel: RiskLevel;
|
|
52
|
+
userAuthorization: UserAuthorization;
|
|
53
|
+
rationale: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Result of asking the reviewer model about one pending tool call.
|
|
58
|
+
* `unavailable` means the review could not be completed at all; it is never
|
|
59
|
+
* treated as approval.
|
|
60
|
+
*/
|
|
61
|
+
export type ReviewOutcome =
|
|
62
|
+
| {
|
|
63
|
+
kind: "allow";
|
|
64
|
+
verdict: GuardianVerdict;
|
|
65
|
+
reviewerModel: string;
|
|
66
|
+
evidenceRounds: number;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
kind: "deny";
|
|
70
|
+
verdict: GuardianVerdict;
|
|
71
|
+
reviewerModel: string;
|
|
72
|
+
evidenceRounds: number;
|
|
73
|
+
}
|
|
74
|
+
| { kind: "unavailable"; reason: string; cause: ReviewFailureCause };
|
|
75
|
+
|
|
76
|
+
/** Non-blocking pre-score produced by the trajectory classifier. */
|
|
77
|
+
export type ClassifierRisk = "low" | "high";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Why the classifier's cached score could or could not satisfy a gated call.
|
|
81
|
+
* Mirrors the deferral reasons the blocking reviewer exists to cover: anything
|
|
82
|
+
* other than `low_risk` falls through to the synchronous review.
|
|
83
|
+
*/
|
|
84
|
+
export type FastDecisionReason =
|
|
85
|
+
| "low_risk"
|
|
86
|
+
| "elevated_risk"
|
|
87
|
+
| "stale_score"
|
|
88
|
+
| "scoring_failure"
|
|
89
|
+
| "missing_score"
|
|
90
|
+
| "authorization_changed"
|
|
91
|
+
| "requires_synchronous_review";
|
|
92
|
+
|
|
93
|
+
export type AutoModeDecisionSource =
|
|
94
|
+
| "reviewer"
|
|
95
|
+
| "classifier"
|
|
96
|
+
| "human"
|
|
97
|
+
| "policy"
|
|
98
|
+
| "circuit-breaker";
|
|
99
|
+
|
|
100
|
+
export type AutoModeDecisionRecord = {
|
|
101
|
+
timestamp: number;
|
|
102
|
+
toolName: string;
|
|
103
|
+
toolCallId: string;
|
|
104
|
+
decision:
|
|
105
|
+
| "allow"
|
|
106
|
+
| "deny"
|
|
107
|
+
| "unavailable-allowed"
|
|
108
|
+
| "unavailable-blocked"
|
|
109
|
+
| "turn-interrupted";
|
|
110
|
+
reason: string;
|
|
111
|
+
reviewerModel?: string;
|
|
112
|
+
source: AutoModeDecisionSource;
|
|
113
|
+
/** Present on reviewer verdicts; omitted for classifier fast paths. */
|
|
114
|
+
riskLevel?: RiskLevel;
|
|
115
|
+
userAuthorization?: UserAuthorization;
|
|
116
|
+
evidenceRounds?: number;
|
|
117
|
+
/** Only present when `source` is `classifier`. */
|
|
118
|
+
fastDecision?: FastDecisionReason;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type AutoModeModelRef = {
|
|
122
|
+
provider?: string;
|
|
123
|
+
id?: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type AutoModeGateSource = AutoModeConfig["gate"];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Truncate to a bounded single-line-safe diagnostic. Reviewer reasons and tool
|
|
130
|
+
* arguments both originate from untrusted text, so every persisted copy is capped.
|
|
131
|
+
*/
|
|
132
|
+
export function boundReviewText(value: unknown, maxChars: number): string {
|
|
133
|
+
const text = typeof value === "string" ? value : String(value ?? "");
|
|
134
|
+
const collapsed = text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim();
|
|
135
|
+
if (collapsed.length <= maxChars) return collapsed;
|
|
136
|
+
return `${collapsed.slice(0, Math.max(0, maxChars - 3))}...`;
|
|
137
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -6,14 +6,32 @@ import {
|
|
|
6
6
|
DEFAULT_COMPACTION_CONFIG,
|
|
7
7
|
DEFAULT_TOOLKIT_CONFIG,
|
|
8
8
|
DEFAULT_AUTO_COMPACTION_CONFIG,
|
|
9
|
+
DEFAULT_AUTO_MODE_CONFIG,
|
|
9
10
|
DEFAULT_IMAGE_GENERATION_CONFIG,
|
|
11
|
+
DEFAULT_NATIVE_FALLBACK_CONFIG,
|
|
10
12
|
DEFAULT_WEB_SEARCH_CONFIG,
|
|
11
13
|
RESPONSES_COMPACT_CAPABLE_APIS,
|
|
14
|
+
BREAKER_LIMIT_MAX,
|
|
15
|
+
BREAKER_LIMIT_MIN,
|
|
16
|
+
BREAKER_WINDOW_MAX,
|
|
17
|
+
BREAKER_WINDOW_MIN,
|
|
18
|
+
CLASSIFIER_MAX_LAG_MAX,
|
|
19
|
+
CLASSIFIER_MAX_LAG_MIN,
|
|
20
|
+
DEFAULT_CLASSIFIER_TIMEOUT_MS,
|
|
21
|
+
EVIDENCE_ROUNDS_MAX,
|
|
22
|
+
EVIDENCE_ROUNDS_MIN,
|
|
23
|
+
REVIEWER_TIMEOUT_MAX_MS,
|
|
24
|
+
REVIEWER_TIMEOUT_MIN_MS,
|
|
12
25
|
THINKING_LEVELS,
|
|
13
26
|
TOOLKIT_ID,
|
|
27
|
+
type AutoModeCircuitBreakerConfig,
|
|
28
|
+
type AutoModeClassifierConfig,
|
|
29
|
+
type AutoModeConfig,
|
|
30
|
+
type AutoModeGate,
|
|
14
31
|
type CompactionConfig,
|
|
15
32
|
type ImageGenerationConfig,
|
|
16
33
|
type LoadedToolkitConfig,
|
|
34
|
+
type NativeFallbackConfig,
|
|
17
35
|
type ToolkitConfig,
|
|
18
36
|
type WebSearchConfig,
|
|
19
37
|
} from "./types";
|
|
@@ -21,13 +39,12 @@ import {
|
|
|
21
39
|
export const CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", TOOLKIT_ID);
|
|
22
40
|
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
23
41
|
|
|
24
|
-
const TOP_LEVEL_FIELDS = new Set(["compaction", "webSearch", "imageGeneration"]);
|
|
42
|
+
const TOP_LEVEL_FIELDS = new Set(["compaction", "webSearch", "imageGeneration", "autoMode"]);
|
|
25
43
|
const COMPACTION_FIELDS = new Set([
|
|
26
44
|
"enabled",
|
|
27
45
|
"allowCompactionContinuityBreak",
|
|
28
46
|
"remoteCompactModel",
|
|
29
|
-
"
|
|
30
|
-
"thinkingLevel",
|
|
47
|
+
"nativeFallback",
|
|
31
48
|
"autoCompaction",
|
|
32
49
|
"responsesApis",
|
|
33
50
|
"notifyOnLoad",
|
|
@@ -37,9 +54,25 @@ const COMPACTION_FIELDS = new Set([
|
|
|
37
54
|
"redactSensitiveData",
|
|
38
55
|
"artifactRoot",
|
|
39
56
|
]);
|
|
57
|
+
const NATIVE_FALLBACK_FIELDS = new Set(["enabled", "model", "thinkingLevel"]);
|
|
40
58
|
const AUTO_COMPACTION_FIELDS = new Set(["enabled", "continuation", "unsupportedFallback", "reserveTokens"]);
|
|
41
59
|
const WEB_SEARCH_FIELDS = new Set(["enabled", "models"]);
|
|
42
|
-
const IMAGE_GENERATION_FIELDS = new Set(["enabled"
|
|
60
|
+
const IMAGE_GENERATION_FIELDS = new Set(["enabled"]);
|
|
61
|
+
const AUTO_MODE_FIELDS = new Set([
|
|
62
|
+
"enabled",
|
|
63
|
+
"models",
|
|
64
|
+
"reviewerModel",
|
|
65
|
+
"gate",
|
|
66
|
+
"extraTools",
|
|
67
|
+
"timeoutMs",
|
|
68
|
+
"transcript",
|
|
69
|
+
"evidenceTools",
|
|
70
|
+
"maxEvidenceRounds",
|
|
71
|
+
"classifier",
|
|
72
|
+
"circuitBreaker",
|
|
73
|
+
]);
|
|
74
|
+
const AUTO_MODE_CLASSIFIER_FIELDS = new Set(["enabled", "model", "timeoutMs", "maxLag"]);
|
|
75
|
+
const AUTO_MODE_BREAKER_FIELDS = new Set(["consecutiveDenials", "recentDenials", "windowSize"]);
|
|
43
76
|
|
|
44
77
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
78
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -148,6 +181,21 @@ function toNonNegativeInteger(value: unknown, fieldPath: string, warnings: strin
|
|
|
148
181
|
return undefined;
|
|
149
182
|
}
|
|
150
183
|
|
|
184
|
+
function toBoundedInteger(
|
|
185
|
+
value: unknown,
|
|
186
|
+
fieldPath: string,
|
|
187
|
+
warnings: string[],
|
|
188
|
+
min: number,
|
|
189
|
+
max: number,
|
|
190
|
+
): number | undefined {
|
|
191
|
+
if (value === undefined) return undefined;
|
|
192
|
+
if (typeof value === "number" && Number.isInteger(value) && Number.isFinite(value) && value >= min && value <= max) {
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
warnings.push(`Ignoring ${fieldPath}: expected an integer between ${min} and ${max}.`);
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
151
199
|
function toSupportedApis(
|
|
152
200
|
value: unknown,
|
|
153
201
|
fieldPath: string,
|
|
@@ -175,7 +223,7 @@ function toSupportedApis(
|
|
|
175
223
|
return accepted;
|
|
176
224
|
}
|
|
177
225
|
|
|
178
|
-
function
|
|
226
|
+
function toStringList(value: unknown, fieldPath: string, warnings: string[]): string[] | undefined {
|
|
179
227
|
if (value === undefined) return undefined;
|
|
180
228
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
181
229
|
warnings.push(`Ignoring ${fieldPath}: expected a string array.`);
|
|
@@ -185,10 +233,34 @@ function toModelAllowlist(value: unknown, fieldPath: string, warnings: string[])
|
|
|
185
233
|
return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
|
|
186
234
|
}
|
|
187
235
|
|
|
236
|
+
function toAutoModeGate(value: unknown, fieldPath: string, warnings: string[]): AutoModeGate | undefined {
|
|
237
|
+
if (value === undefined) return undefined;
|
|
238
|
+
if (value === "side-effect" || value === "all") return value;
|
|
239
|
+
warnings.push(`Ignoring ${fieldPath}: expected one of side-effect, all.`);
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function toReviewerTimeoutMs(value: unknown, fieldPath: string, warnings: string[]): number | undefined {
|
|
244
|
+
if (value === undefined) return undefined;
|
|
245
|
+
if (
|
|
246
|
+
typeof value === "number" &&
|
|
247
|
+
Number.isInteger(value) &&
|
|
248
|
+
value >= REVIEWER_TIMEOUT_MIN_MS &&
|
|
249
|
+
value <= REVIEWER_TIMEOUT_MAX_MS
|
|
250
|
+
) {
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
warnings.push(
|
|
254
|
+
`Ignoring ${fieldPath}: expected an integer between ${REVIEWER_TIMEOUT_MIN_MS} and ${REVIEWER_TIMEOUT_MAX_MS} ms.`,
|
|
255
|
+
);
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
188
259
|
function cloneDefaults(): ToolkitConfig {
|
|
189
260
|
return {
|
|
190
261
|
compaction: {
|
|
191
262
|
...DEFAULT_COMPACTION_CONFIG,
|
|
263
|
+
nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
|
|
192
264
|
autoCompaction: { ...DEFAULT_AUTO_COMPACTION_CONFIG },
|
|
193
265
|
responsesApis: [...DEFAULT_COMPACTION_CONFIG.responsesApis],
|
|
194
266
|
},
|
|
@@ -196,13 +268,36 @@ function cloneDefaults(): ToolkitConfig {
|
|
|
196
268
|
...DEFAULT_WEB_SEARCH_CONFIG,
|
|
197
269
|
models: [...DEFAULT_WEB_SEARCH_CONFIG.models],
|
|
198
270
|
},
|
|
199
|
-
imageGeneration: {
|
|
200
|
-
|
|
201
|
-
|
|
271
|
+
imageGeneration: { ...DEFAULT_IMAGE_GENERATION_CONFIG },
|
|
272
|
+
autoMode: {
|
|
273
|
+
...DEFAULT_AUTO_MODE_CONFIG,
|
|
274
|
+
models: [...DEFAULT_AUTO_MODE_CONFIG.models],
|
|
275
|
+
extraTools: [...DEFAULT_AUTO_MODE_CONFIG.extraTools],
|
|
276
|
+
classifier: { ...DEFAULT_AUTO_MODE_CONFIG.classifier },
|
|
277
|
+
circuitBreaker: { ...DEFAULT_AUTO_MODE_CONFIG.circuitBreaker },
|
|
202
278
|
},
|
|
203
279
|
};
|
|
204
280
|
}
|
|
205
281
|
|
|
282
|
+
function applyNativeFallbackConfig(
|
|
283
|
+
raw: Record<string, unknown>,
|
|
284
|
+
resolved: NativeFallbackConfig,
|
|
285
|
+
warnings: string[],
|
|
286
|
+
): void {
|
|
287
|
+
warnUnknownFields(raw, NATIVE_FALLBACK_FIELDS, "compaction.nativeFallback", warnings);
|
|
288
|
+
resolved.enabled =
|
|
289
|
+
toBoolean(raw.enabled, "compaction.nativeFallback.enabled", warnings) ?? resolved.enabled;
|
|
290
|
+
|
|
291
|
+
const modelSpec = toModelSpec(raw.model, "compaction.nativeFallback.model", warnings);
|
|
292
|
+
if (modelSpec !== undefined) {
|
|
293
|
+
resolved.model = modelSpec === null ? undefined : modelSpec;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
resolved.thinkingLevel =
|
|
297
|
+
toThinkingLevel(raw.thinkingLevel, "compaction.nativeFallback.thinkingLevel", warnings) ??
|
|
298
|
+
resolved.thinkingLevel;
|
|
299
|
+
}
|
|
300
|
+
|
|
206
301
|
function applyAutoCompactionConfig(
|
|
207
302
|
raw: Record<string, unknown>,
|
|
208
303
|
resolved: CompactionConfig["autoCompaction"],
|
|
@@ -265,14 +360,14 @@ function applyCompactionConfig(
|
|
|
265
360
|
resolved.remoteCompactModel = remoteCompactModelSpec === null ? undefined : remoteCompactModelSpec;
|
|
266
361
|
}
|
|
267
362
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
363
|
+
if (raw.nativeFallback !== undefined) {
|
|
364
|
+
if (isRecord(raw.nativeFallback)) {
|
|
365
|
+
applyNativeFallbackConfig(raw.nativeFallback, resolved.nativeFallback, warnings);
|
|
366
|
+
} else {
|
|
367
|
+
warnings.push("Ignoring compaction.nativeFallback: expected a JSON object.");
|
|
368
|
+
}
|
|
271
369
|
}
|
|
272
370
|
|
|
273
|
-
resolved.thinkingLevel =
|
|
274
|
-
toThinkingLevel(raw.thinkingLevel, "compaction.thinkingLevel", warnings) ?? resolved.thinkingLevel;
|
|
275
|
-
|
|
276
371
|
if (raw.autoCompaction !== undefined) {
|
|
277
372
|
if (isRecord(raw.autoCompaction)) {
|
|
278
373
|
applyAutoCompactionConfig(raw.autoCompaction, resolved.autoCompaction, warnings);
|
|
@@ -306,7 +401,7 @@ function applyWebSearchConfig(
|
|
|
306
401
|
warnUnknownFields(raw, WEB_SEARCH_FIELDS, "webSearch", warnings);
|
|
307
402
|
resolved.enabled = toBoolean(raw.enabled, "webSearch.enabled", warnings) ?? resolved.enabled;
|
|
308
403
|
|
|
309
|
-
const models =
|
|
404
|
+
const models = toStringList(raw.models, "webSearch.models", warnings);
|
|
310
405
|
if (models !== undefined) {
|
|
311
406
|
resolved.models = models;
|
|
312
407
|
}
|
|
@@ -320,11 +415,147 @@ function applyImageGenerationConfig(
|
|
|
320
415
|
warnUnknownFields(raw, IMAGE_GENERATION_FIELDS, "imageGeneration", warnings);
|
|
321
416
|
resolved.enabled =
|
|
322
417
|
toBoolean(raw.enabled, "imageGeneration.enabled", warnings) ?? resolved.enabled;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function applyAutoModeConfig(
|
|
421
|
+
raw: Record<string, unknown>,
|
|
422
|
+
resolved: AutoModeConfig,
|
|
423
|
+
warnings: string[],
|
|
424
|
+
): void {
|
|
425
|
+
warnUnknownFields(raw, AUTO_MODE_FIELDS, "autoMode", warnings);
|
|
426
|
+
resolved.enabled = toBoolean(raw.enabled, "autoMode.enabled", warnings) ?? resolved.enabled;
|
|
323
427
|
|
|
324
|
-
const models =
|
|
428
|
+
const models = toStringList(raw.models, "autoMode.models", warnings);
|
|
325
429
|
if (models !== undefined) {
|
|
326
430
|
resolved.models = models;
|
|
327
431
|
}
|
|
432
|
+
|
|
433
|
+
const reviewerModel = toModelSpec(raw.reviewerModel, "autoMode.reviewerModel", warnings);
|
|
434
|
+
if (reviewerModel !== undefined) {
|
|
435
|
+
resolved.reviewerModel = reviewerModel === null ? undefined : reviewerModel;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
resolved.gate = toAutoModeGate(raw.gate, "autoMode.gate", warnings) ?? resolved.gate;
|
|
439
|
+
|
|
440
|
+
const extraTools = toStringList(raw.extraTools, "autoMode.extraTools", warnings);
|
|
441
|
+
if (extraTools !== undefined) {
|
|
442
|
+
resolved.extraTools = extraTools;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const timeoutMs = toReviewerTimeoutMs(raw.timeoutMs, "autoMode.timeoutMs", warnings);
|
|
446
|
+
if (timeoutMs !== undefined) {
|
|
447
|
+
resolved.timeoutMs = timeoutMs;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
resolved.transcript = toBoolean(raw.transcript, "autoMode.transcript", warnings) ?? resolved.transcript;
|
|
451
|
+
resolved.evidenceTools =
|
|
452
|
+
toBoolean(raw.evidenceTools, "autoMode.evidenceTools", warnings) ?? resolved.evidenceTools;
|
|
453
|
+
|
|
454
|
+
const maxEvidenceRounds = toBoundedInteger(
|
|
455
|
+
raw.maxEvidenceRounds,
|
|
456
|
+
"autoMode.maxEvidenceRounds",
|
|
457
|
+
warnings,
|
|
458
|
+
EVIDENCE_ROUNDS_MIN,
|
|
459
|
+
EVIDENCE_ROUNDS_MAX,
|
|
460
|
+
);
|
|
461
|
+
if (maxEvidenceRounds !== undefined) {
|
|
462
|
+
resolved.maxEvidenceRounds = maxEvidenceRounds;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (raw.classifier !== undefined) {
|
|
466
|
+
if (isRecord(raw.classifier)) {
|
|
467
|
+
applyAutoModeClassifierConfig(raw.classifier, resolved.classifier, warnings);
|
|
468
|
+
} else {
|
|
469
|
+
warnings.push("Ignoring autoMode.classifier: expected a JSON object.");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (raw.circuitBreaker !== undefined) {
|
|
474
|
+
if (isRecord(raw.circuitBreaker)) {
|
|
475
|
+
applyAutoModeBreakerConfig(raw.circuitBreaker, resolved.circuitBreaker, warnings);
|
|
476
|
+
} else {
|
|
477
|
+
warnings.push("Ignoring autoMode.circuitBreaker: expected a JSON object.");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function applyAutoModeClassifierConfig(
|
|
483
|
+
raw: Record<string, unknown>,
|
|
484
|
+
resolved: AutoModeClassifierConfig,
|
|
485
|
+
warnings: string[],
|
|
486
|
+
): void {
|
|
487
|
+
warnUnknownFields(raw, AUTO_MODE_CLASSIFIER_FIELDS, "autoMode.classifier", warnings);
|
|
488
|
+
resolved.enabled =
|
|
489
|
+
toBoolean(raw.enabled, "autoMode.classifier.enabled", warnings) ?? resolved.enabled;
|
|
490
|
+
|
|
491
|
+
const model = toModelSpec(raw.model, "autoMode.classifier.model", warnings);
|
|
492
|
+
if (model !== undefined) {
|
|
493
|
+
resolved.model = model === null ? undefined : model;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const timeoutMs =
|
|
497
|
+
toBoundedInteger(
|
|
498
|
+
raw.timeoutMs,
|
|
499
|
+
"autoMode.classifier.timeoutMs",
|
|
500
|
+
warnings,
|
|
501
|
+
REVIEWER_TIMEOUT_MIN_MS,
|
|
502
|
+
REVIEWER_TIMEOUT_MAX_MS,
|
|
503
|
+
) ?? DEFAULT_CLASSIFIER_TIMEOUT_MS;
|
|
504
|
+
if (raw.timeoutMs !== undefined) {
|
|
505
|
+
resolved.timeoutMs = timeoutMs;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const maxLag = toBoundedInteger(
|
|
509
|
+
raw.maxLag,
|
|
510
|
+
"autoMode.classifier.maxLag",
|
|
511
|
+
warnings,
|
|
512
|
+
CLASSIFIER_MAX_LAG_MIN,
|
|
513
|
+
CLASSIFIER_MAX_LAG_MAX,
|
|
514
|
+
);
|
|
515
|
+
if (maxLag !== undefined) {
|
|
516
|
+
resolved.maxLag = maxLag;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function applyAutoModeBreakerConfig(
|
|
521
|
+
raw: Record<string, unknown>,
|
|
522
|
+
resolved: AutoModeCircuitBreakerConfig,
|
|
523
|
+
warnings: string[],
|
|
524
|
+
): void {
|
|
525
|
+
warnUnknownFields(raw, AUTO_MODE_BREAKER_FIELDS, "autoMode.circuitBreaker", warnings);
|
|
526
|
+
|
|
527
|
+
const consecutiveDenials = toBoundedInteger(
|
|
528
|
+
raw.consecutiveDenials,
|
|
529
|
+
"autoMode.circuitBreaker.consecutiveDenials",
|
|
530
|
+
warnings,
|
|
531
|
+
BREAKER_LIMIT_MIN,
|
|
532
|
+
BREAKER_LIMIT_MAX,
|
|
533
|
+
);
|
|
534
|
+
if (consecutiveDenials !== undefined) {
|
|
535
|
+
resolved.consecutiveDenials = consecutiveDenials;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const recentDenials = toBoundedInteger(
|
|
539
|
+
raw.recentDenials,
|
|
540
|
+
"autoMode.circuitBreaker.recentDenials",
|
|
541
|
+
warnings,
|
|
542
|
+
BREAKER_LIMIT_MIN,
|
|
543
|
+
BREAKER_LIMIT_MAX,
|
|
544
|
+
);
|
|
545
|
+
if (recentDenials !== undefined) {
|
|
546
|
+
resolved.recentDenials = recentDenials;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const windowSize = toBoundedInteger(
|
|
550
|
+
raw.windowSize,
|
|
551
|
+
"autoMode.circuitBreaker.windowSize",
|
|
552
|
+
warnings,
|
|
553
|
+
BREAKER_WINDOW_MIN,
|
|
554
|
+
BREAKER_WINDOW_MAX,
|
|
555
|
+
);
|
|
556
|
+
if (windowSize !== undefined) {
|
|
557
|
+
resolved.windowSize = windowSize;
|
|
558
|
+
}
|
|
328
559
|
}
|
|
329
560
|
|
|
330
561
|
/**
|
|
@@ -365,6 +596,14 @@ export function loadToolkitConfig(configPath: string = CONFIG_PATH): LoadedToolk
|
|
|
365
596
|
warnings.push("Ignoring imageGeneration: expected a JSON object.");
|
|
366
597
|
}
|
|
367
598
|
}
|
|
599
|
+
|
|
600
|
+
if (raw.autoMode !== undefined) {
|
|
601
|
+
if (isRecord(raw.autoMode)) {
|
|
602
|
+
applyAutoModeConfig(raw.autoMode, resolved.autoMode, warnings);
|
|
603
|
+
} else {
|
|
604
|
+
warnings.push("Ignoring autoMode: expected a JSON object.");
|
|
605
|
+
}
|
|
606
|
+
}
|
|
368
607
|
}
|
|
369
608
|
|
|
370
609
|
resolved.compaction.artifactRoot = resolveConfiguredPath(
|
package/src/extension-runtime.ts
CHANGED
|
@@ -316,6 +316,7 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
// Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
|
|
319
|
+
let remoteAttempted = false;
|
|
319
320
|
const resolution = await resolveRemoteCompactionExecution(
|
|
320
321
|
ctx,
|
|
321
322
|
{
|
|
@@ -325,6 +326,7 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
325
326
|
config.remoteCompactModel,
|
|
326
327
|
);
|
|
327
328
|
if (resolution.ok) {
|
|
329
|
+
remoteAttempted = true;
|
|
328
330
|
const responsesOutcome = await runResponsesNativeCompact(event, ctx, config, resolution.execution);
|
|
329
331
|
if (responsesOutcome.outcome === "success") {
|
|
330
332
|
return { compaction: responsesOutcome.compaction };
|
|
@@ -357,8 +359,10 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
357
359
|
}
|
|
358
360
|
}
|
|
359
361
|
|
|
360
|
-
// Branch 2: run pi's native compaction method
|
|
361
|
-
|
|
362
|
+
// Branch 2: run pi's native compaction method. A failed remote request is compacted by the
|
|
363
|
+
// remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
|
|
364
|
+
const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
|
|
365
|
+
const fallback = await runNativeFallbackCompaction({ ctx, event, config, modelSpec: fallbackModelSpec });
|
|
362
366
|
if (fallback.ok) {
|
|
363
367
|
if (ctx.hasUI) {
|
|
364
368
|
ctx.ui.notify(
|
|
@@ -394,8 +398,12 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
394
398
|
ctx,
|
|
395
399
|
);
|
|
396
400
|
|
|
397
|
-
// Intentional pi-default paths:
|
|
398
|
-
|
|
401
|
+
// Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
|
|
402
|
+
const intentionalSkip =
|
|
403
|
+
fallback.reason === "disabled" ||
|
|
404
|
+
fallback.reason === "no-model-configured" ||
|
|
405
|
+
fallback.reason === "same-as-current-model";
|
|
406
|
+
if (!intentionalSkip) {
|
|
399
407
|
notifyWarning(
|
|
400
408
|
ctx,
|
|
401
409
|
`compaction model "${fallback.modelSpec}" unusable (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); using pi's default compaction`,
|