Enhance agent context handling and feature store options

- Updated the `runAgent` function to include additional parameters for context management, ensuring isolated execution for subagents.
- Introduced a new `parseContextLabel` function to convert context size labels into token counts, improving flexibility for model configurations.
- Enhanced the `optionsFor` method in `FeatureStore` to ensure every model has a context window option, providing a fallback for models without preset context values.
- Refactored sidebar provider logic to utilize the new context parsing function, streamlining context size handling across the application.
This commit is contained in:
Pawan Osman
2026-07-15 14:21:53 +03:00
parent 1baa6f8c58
commit 268b0e28de
3 changed files with 60 additions and 23 deletions
+7 -2
View File
@@ -96,6 +96,8 @@ export async function runAgent(opts: RunAgentOptions): Promise<void> {
});
}
let finalText = "";
// Isolated context: empty history, own budget, no parent chat steps.
// Parent only receives the final summary string (run-result), never sub history.
const runP = runAgent({
apiBaseUrl,
apiKey,
@@ -103,12 +105,16 @@ export async function runAgent(opts: RunAgentOptions): Promise<void> {
mode: subReadonly ? "ask" : "agent",
prompt: subPrompt,
history: [],
maxTokens,
contextTokens,
sampling,
modelParams,
anthropic,
oauthKind,
systemPromptOverride: subSystemOverride,
enableFileReading,
enableTerminalSuggestions,
enableWorkspaceContext,
approve,
isSubagent: true,
signal: childAC.signal,
@@ -116,8 +122,7 @@ export async function runAgent(opts: RunAgentOptions): Promise<void> {
if (e.type === "run-result") {
finalText = e.text;
}
// Forward the subagent's stream to the parent so the UI can render it
// as a nested read-only sub-chat keyed by the task call id.
// UI-only stream; never re-inject subagent steps into parent history.
if (callId) {
emit({ type: "subagent-event", callId, event: e });
}
+51 -14
View File
@@ -101,6 +101,29 @@ const effort = (value = "medium", values = ["none", "low", "medium", "high"]): M
const thinking = (value = "adaptive", values = ["disabled", "adaptive", "enabled"]): ModelOption => ({ key: "thinking", label: "Thinking", type: "select", values, value });
const ctx = (values: string[], value: string): ModelOption => ({ key: "max_context", label: "Context", type: "select", values, value });
/** Fallback context sizes for models with no catalog preset (custom / fetched). */
export const DEFAULT_CONTEXT_VALUES = ["32k", "64k", "128k", "200k", "256k", "512k", "1m"];
export const DEFAULT_CONTEXT_VALUE = "128k";
export const defaultContextOption = (): ModelOption =>
ctx([...DEFAULT_CONTEXT_VALUES], DEFAULT_CONTEXT_VALUE);
/** Parse "200k" / "1m" / "128000" → token count. */
export function parseContextLabel(v?: string): number {
if (!v) return 0;
const s = String(v).trim().toLowerCase();
const m = s.match(/^([\d.]+)\s*([kmb])?$/);
if (!m) {
const n = Number(s.replace(/[^\d.]/g, ""));
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
}
const n = parseFloat(m[1]);
const u = m[2];
if (u === "m") return Math.floor(n * 1_000_000);
if (u === "b") return Math.floor(n * 1_000_000_000);
if (u === "k") return Math.floor(n * 1_000);
return Math.floor(n);
}
/** Built-in catalog of popular coding models. Users can edit options per model. */
export const MODEL_CATALOG: ModelDef[] = [
// OpenAI — gpt-5.5 is the current flagship; effort supports none/low/medium/high (xhigh on top tiers).
@@ -324,24 +347,38 @@ export class FeatureStore {
/** Resolved options for a model: stored overrides take precedence over defaults.
* Overrides are kind-scoped ("<kind>:<id>") so the same model id can hold
* different option state per provider (e.g. anthropic vs claude-code);
* a plain-id record is the legacy/shared fallback. */
* a plain-id record is the legacy/shared fallback.
* Models with no catalog context option get a default max_context selector. */
optionsFor(modelId: string, kind?: string): ModelOption[] {
const cfg = this.get();
const def = this.defFor(modelId, kind);
const saved = (kind ? cfg.modelOptions[`${kind}:${modelId}`] : undefined) ?? cfg.modelOptions[modelId];
if (!saved) return def?.options ?? [];
if (!def?.options) return saved;
// Merge: option shape (label/type/values = model capabilities) always comes
// from the current catalog; only the user's selected `value` is persisted.
// This keeps stale saved options from hiding newly-added modes/values.
const savedValue = new Map(saved.map((o) => [o.key, o.value]));
return def.options.map((o) => {
const v = savedValue.get(o.key);
if (v == null) return o;
// Drop a saved value that's no longer a valid choice for this option.
if (o.values && !o.values.includes(v)) return o;
return { ...o, value: v };
});
const base: ModelOption[] = (() => {
if (!saved) return def?.options ? [...def.options] : [];
if (!def?.options) return [...saved];
// Merge: option shape (label/type/values = model capabilities) always comes
// from the current catalog; only the user's selected `value` is persisted.
const savedValue = new Map(saved.map((o) => [o.key, o.value]));
return def.options.map((o) => {
const v = savedValue.get(o.key);
if (v == null) return o;
if (o.values && !o.values.includes(v)) return o;
return { ...o, value: v };
});
})();
// Ensure every model exposes a context window (catalog or fallback dropdown).
if (!base.some((o) => o.key === "max_context")) {
const savedCtx = saved?.find((o) => o.key === "max_context")?.value;
const fallback = defaultContextOption();
if (savedCtx && fallback.values?.includes(savedCtx)) fallback.value = savedCtx;
else if (savedCtx) {
// Keep a custom value the user typed/saved even if not in the list.
fallback.values = [...(fallback.values || []), savedCtx];
fallback.value = savedCtx;
}
base.push(fallback);
}
return base;
}
/** Friendly catalog label for an id, or the id itself if not catalogued. */
+2 -7
View File
@@ -14,7 +14,7 @@ import { AgentEvent, Mode, Attachment } from "../agent/types";
import { listModels, generateTitle, pickModel } from "../agent/provider";
import { renderWebviewHtml } from "./webviewHtml";
import { ConversationStore, titleFromText } from "../stores/conversationStore";
import { FeatureStore, MODEL_CATALOG, kindMatches, optionsToParams, providerEnabled, type ModelDef, type ModelOption, type ProviderConfig } from "../stores/featureStore";
import { FeatureStore, MODEL_CATALOG, kindMatches, optionsToParams, parseContextLabel, providerEnabled, type ModelDef, type ModelOption, type ProviderConfig } from "../stores/featureStore";
import { effectiveContextLength, ensureLoaded, isRunning, serverUrlFor } from "../agent/llamacpp";
import * as ollama from "../agent/ollama";
import * as oauth from "../agent/oauth";
@@ -937,12 +937,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
const m = f.llamacppModels.find((x) => x.id === modelId || x.id === bare);
if (m) return effectiveContextLength(m, f.llamacppContextLength);
const opt = this.featureStore.optionsFor(bare, oauthKind).find((o) => o.key === "max_context")?.value;
const parsed = /^([\d.]+)\s*([km])?$/i.exec((opt || "").trim());
if (parsed) {
const unit = (parsed[2] || "").toLowerCase();
return Math.round(parseFloat(parsed[1]) * (unit === "m" ? 1_000_000 : unit === "k" ? 1_000 : 1));
}
return 200_000; // ponytail: safe default; refine per-provider when model metadata is available
return parseContextLabel(opt) || 128_000;
}
/** Build the picker model list from ALL enabled providers. */