mirror of
https://github.com/volcengine/OpenViking.git
synced 2026-09-30 17:28:07 +08:00
Coding-agent plugins capped a tool part's `tool_output` at 2000 chars before
POSTing it to `/api/v1/sessions/{id}/messages`. That cap sits below the server's
own externalization threshold (`tool_output_externalization.threshold_chars`,
default 20000), so output in the 2k-20k band was destroyed for no reason and
anything larger never reached `ToolResultStore` - leaving `tool_output_ref`
permanently empty and the `/tool-results` read-back path unusable.
Raise the `captureToolMaxChars` default to 1000000 (a guard against pathological
payloads, not a truncation policy) and lift the opencode/pi clamps that would
otherwise pin it back to 20000. claude-code had no knob at all - two hardcoded
`TOOL_OUTPUT_PART_MAX_CHARS = 2000` constants - so it gains the same config
entry and both capture scripts now read it.
Also stop pi from sending tool output twice: for a tool-only payload the
rawText-derived text part re-rendered the same output the tool part carries.
191 lines
8.8 KiB
TypeScript
191 lines
8.8 KiB
TypeScript
import { readFileSync, existsSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { buildUserAgent, resolveOpenVikingCredentials } from "./shared/credentials.mjs";
|
|
import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs";
|
|
|
|
/** Hand-maintained: this extension ships no manifest to read a version from. */
|
|
export const EXTENSION_VERSION = "0.1.0";
|
|
|
|
export interface OVConfig {
|
|
enabled: boolean;
|
|
endpoint: string;
|
|
apiKey: string;
|
|
account: string;
|
|
user: string;
|
|
peerId: string;
|
|
userAgent: string;
|
|
workspacePeer: boolean;
|
|
recallPeerScope: "actor" | "all";
|
|
recallQueryExpansion: "auto" | "off";
|
|
recallQueryExpansionConfigured: boolean;
|
|
syncTurns: boolean;
|
|
recallTokenBudget: number;
|
|
recallMaxContentChars: number;
|
|
recallPreferAbstract: boolean;
|
|
recallLimit: number;
|
|
recallLimitConfigured: boolean;
|
|
scoreThreshold: number;
|
|
minQueryLength: number;
|
|
profileTokenBudget: number;
|
|
resumeContextBudget: number;
|
|
commitTokenThreshold: number;
|
|
commitKeepRecentCount: number;
|
|
takeoverEnabled: boolean;
|
|
takeoverTokenThreshold: number;
|
|
takeoverKeepRecentTurns: number;
|
|
takeoverOverviewBudget: number;
|
|
takeoverOverviewPollMs: number;
|
|
takeoverOverviewPollMax: number;
|
|
captureToolResults: boolean;
|
|
captureMode: "semantic" | "keyword";
|
|
captureMaxLength: number;
|
|
captureToolMaxChars: number;
|
|
captureAssistantTurns: boolean;
|
|
bypassPatterns: string[];
|
|
logLevel: "silent" | "error" | "info";
|
|
}
|
|
|
|
const DEFAULT_CONFIG: OVConfig = {
|
|
enabled: true,
|
|
endpoint: "http://127.0.0.1:1933",
|
|
apiKey: "",
|
|
account: "",
|
|
user: "",
|
|
peerId: "",
|
|
userAgent: "",
|
|
workspacePeer: true,
|
|
recallPeerScope: "all",
|
|
// Server-side query expansion costs a model call before retrieval starts, so
|
|
// it has to be switchable from the client that pays the latency.
|
|
recallQueryExpansion: "auto",
|
|
recallQueryExpansionConfigured: false,
|
|
syncTurns: true,
|
|
recallTokenBudget: 2000,
|
|
recallMaxContentChars: 500,
|
|
recallPreferAbstract: true,
|
|
recallLimit: 10,
|
|
recallLimitConfigured: false,
|
|
scoreThreshold: 0.35,
|
|
minQueryLength: 3,
|
|
profileTokenBudget: 10000,
|
|
resumeContextBudget: 32000,
|
|
commitTokenThreshold: 20000,
|
|
commitKeepRecentCount: 10,
|
|
takeoverEnabled: true,
|
|
takeoverTokenThreshold: 30000,
|
|
takeoverKeepRecentTurns: 3,
|
|
takeoverOverviewBudget: 3000,
|
|
takeoverOverviewPollMs: 2000,
|
|
takeoverOverviewPollMax: 15,
|
|
captureToolResults: false,
|
|
captureMode: "semantic",
|
|
captureMaxLength: 24000,
|
|
captureToolMaxChars: 1000000,
|
|
captureAssistantTurns: true,
|
|
bypassPatterns: [],
|
|
logLevel: "error",
|
|
};
|
|
|
|
export function loadConfigFromModuleUrl(moduleUrl: string): OVConfig {
|
|
return loadConfig(dirname(fileURLToPath(moduleUrl)));
|
|
}
|
|
|
|
export function loadConfig(extensionDir: string): OVConfig {
|
|
const configPath = join(extensionDir, "config.json");
|
|
let file: any = {};
|
|
try {
|
|
if (existsSync(configPath)) file = JSON.parse(readFileSync(configPath, "utf8"));
|
|
} catch {
|
|
file = {};
|
|
}
|
|
|
|
const takeover = file.takeover && typeof file.takeover === "object" ? file.takeover : {};
|
|
const creds = resolveOpenVikingCredentials();
|
|
const config: OVConfig = {
|
|
...DEFAULT_CONFIG,
|
|
...file,
|
|
endpoint: creds.baseUrl,
|
|
apiKey: creds.apiKey,
|
|
account: creds.account,
|
|
user: creds.user,
|
|
peerId: creds.peerId,
|
|
userAgent: buildUserAgent("pi", EXTENSION_VERSION),
|
|
recallLimitConfigured: Object.prototype.hasOwnProperty.call(file, "recallLimit"),
|
|
recallQueryExpansionConfigured: Object.prototype.hasOwnProperty.call(file, "recallQueryExpansion"),
|
|
recallTokenBudget: file.recallTokenBudget ?? file.recallBudget ?? DEFAULT_CONFIG.recallTokenBudget,
|
|
scoreThreshold: file.scoreThreshold ?? file.recallScoreThreshold ?? DEFAULT_CONFIG.scoreThreshold,
|
|
minQueryLength: file.minQueryLength ?? file.recallMinQueryLength ?? DEFAULT_CONFIG.minQueryLength,
|
|
profileTokenBudget: file.profileTokenBudget ?? file.profileBudget ?? DEFAULT_CONFIG.profileTokenBudget,
|
|
takeoverEnabled: takeover.enabled ?? file.takeoverEnabled ?? DEFAULT_CONFIG.takeoverEnabled,
|
|
takeoverTokenThreshold: takeover.tokenThreshold ?? file.takeoverTokenThreshold ?? DEFAULT_CONFIG.takeoverTokenThreshold,
|
|
takeoverKeepRecentTurns: takeover.keepRecentTurns ?? file.takeoverKeepRecentTurns ?? DEFAULT_CONFIG.takeoverKeepRecentTurns,
|
|
takeoverOverviewBudget: takeover.overviewBudget ?? file.takeoverOverviewBudget ?? DEFAULT_CONFIG.takeoverOverviewBudget,
|
|
takeoverOverviewPollMs: takeover.overviewPollMs ?? file.takeoverOverviewPollMs ?? DEFAULT_CONFIG.takeoverOverviewPollMs,
|
|
takeoverOverviewPollMax: takeover.overviewPollMax ?? file.takeoverOverviewPollMax ?? DEFAULT_CONFIG.takeoverOverviewPollMax,
|
|
};
|
|
|
|
if (process.env.OPENVIKING_URL || process.env.OPENVIKING_BASE_URL) config.endpoint = creds.baseUrl;
|
|
if (process.env.OPENVIKING_API_KEY || process.env.OPENVIKING_BEARER_TOKEN) config.apiKey = creds.apiKey;
|
|
if (process.env.OPENVIKING_ACCOUNT) config.account = creds.account;
|
|
if (process.env.OPENVIKING_USER) config.user = creds.user;
|
|
if (process.env.OPENVIKING_PEER_ID) config.peerId = creds.peerId;
|
|
if (process.env.OPENVIKING_WORKSPACE_PEER !== undefined) {
|
|
config.workspacePeer = envBool(process.env.OPENVIKING_WORKSPACE_PEER, config.workspacePeer);
|
|
}
|
|
if (process.env.OPENVIKING_RECALL_PEER_SCOPE) {
|
|
config.recallPeerScope = process.env.OPENVIKING_RECALL_PEER_SCOPE === "actor" ? "actor" : "all";
|
|
}
|
|
if (process.env.OPENVIKING_RECALL_LIMIT) {
|
|
config.recallLimit = Number(process.env.OPENVIKING_RECALL_LIMIT);
|
|
config.recallLimitConfigured = true;
|
|
}
|
|
if (process.env.OPENVIKING_RECALL_QUERY_EXPANSION) {
|
|
config.recallQueryExpansion = process.env.OPENVIKING_RECALL_QUERY_EXPANSION === "off" ? "off" : "auto";
|
|
config.recallQueryExpansionConfigured = true;
|
|
}
|
|
|
|
config.recallLimit = clampInt(config.recallLimit, 1, 50, DEFAULT_CONFIG.recallLimit);
|
|
config.recallMaxContentChars = clampInt(config.recallMaxContentChars, 100, 5000, DEFAULT_CONFIG.recallMaxContentChars);
|
|
config.recallTokenBudget = clampInt(config.recallTokenBudget, 200, 50000, DEFAULT_CONFIG.recallTokenBudget);
|
|
config.scoreThreshold = clampNumber(config.scoreThreshold, 0, 1, DEFAULT_CONFIG.scoreThreshold);
|
|
config.minQueryLength = clampInt(config.minQueryLength, 1, 64, DEFAULT_CONFIG.minQueryLength);
|
|
config.profileTokenBudget = clampInt(config.profileTokenBudget, 500, 50000, DEFAULT_CONFIG.profileTokenBudget);
|
|
config.resumeContextBudget = clampInt(config.resumeContextBudget, 1024, 128000, DEFAULT_CONFIG.resumeContextBudget);
|
|
config.commitTokenThreshold = clampInt(config.commitTokenThreshold, 1000, 1000000, DEFAULT_CONFIG.commitTokenThreshold);
|
|
config.commitKeepRecentCount = clampInt(config.commitKeepRecentCount, 0, 1000, DEFAULT_CONFIG.commitKeepRecentCount);
|
|
config.takeoverEnabled = config.takeoverEnabled !== false;
|
|
config.takeoverTokenThreshold = clampInt(config.takeoverTokenThreshold, 1, 1000000, DEFAULT_CONFIG.takeoverTokenThreshold);
|
|
config.takeoverKeepRecentTurns = clampInt(config.takeoverKeepRecentTurns, 0, 100, DEFAULT_CONFIG.takeoverKeepRecentTurns);
|
|
config.takeoverOverviewBudget = clampInt(config.takeoverOverviewBudget, 100, 50000, DEFAULT_CONFIG.takeoverOverviewBudget);
|
|
config.takeoverOverviewPollMs = clampInt(config.takeoverOverviewPollMs, 0, 60000, DEFAULT_CONFIG.takeoverOverviewPollMs);
|
|
config.takeoverOverviewPollMax = clampInt(config.takeoverOverviewPollMax, 1, 120, DEFAULT_CONFIG.takeoverOverviewPollMax);
|
|
config.captureMaxLength = clampInt(config.captureMaxLength, 200, 100000, DEFAULT_CONFIG.captureMaxLength);
|
|
config.captureToolMaxChars = clampInt(config.captureToolMaxChars, 200, 1000000, DEFAULT_CONFIG.captureToolMaxChars);
|
|
config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic";
|
|
config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all";
|
|
config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto";
|
|
if (!Array.isArray(config.bypassPatterns)) config.bypassPatterns = [];
|
|
config.peerId = resolveEffectivePeerId({ cfg: config as any, cwd: process.cwd() }).peerId;
|
|
return config;
|
|
}
|
|
|
|
function envBool(value: string, fallback: boolean): boolean {
|
|
const lower = String(value || "").trim().toLowerCase();
|
|
if (lower === "0" || lower === "false" || lower === "no" || lower === "off") return false;
|
|
if (lower === "1" || lower === "true" || lower === "yes" || lower === "on") return true;
|
|
return fallback;
|
|
}
|
|
|
|
function clampInt(value: unknown, min: number, max: number, fallback: number): number {
|
|
const next = Math.round(Number(value));
|
|
if (!Number.isFinite(next)) return fallback;
|
|
return Math.max(min, Math.min(max, next));
|
|
}
|
|
|
|
function clampNumber(value: unknown, min: number, max: number, fallback: number): number {
|
|
const next = Number(value);
|
|
if (!Number.isFinite(next)) return fallback;
|
|
return Math.max(min, Math.min(max, next));
|
|
}
|