From 268b0e28dec32977b3e8237e7eeca8986b05766f Mon Sep 17 00:00:00 2001 From: Pawan Osman Date: Wed, 15 Jul 2026 14:21:53 +0300 Subject: [PATCH] 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. --- src/agent/loop.ts | 9 ++++-- src/stores/featureStore.ts | 65 ++++++++++++++++++++++++++++++-------- src/ui/sidebarProvider.ts | 9 ++---- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/agent/loop.ts b/src/agent/loop.ts index 517c1e4..6d96fd1 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -96,6 +96,8 @@ export async function runAgent(opts: RunAgentOptions): Promise { }); } 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 { 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 { 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 }); } diff --git a/src/stores/featureStore.ts b/src/stores/featureStore.ts index f494437..c1d93a2 100644 --- a/src/stores/featureStore.ts +++ b/src/stores/featureStore.ts @@ -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 (":") 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. */ diff --git a/src/ui/sidebarProvider.ts b/src/ui/sidebarProvider.ts index b9235d8..348a989 100644 --- a/src/ui/sidebarProvider.ts +++ b/src/ui/sidebarProvider.ts @@ -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. */