Type runtime config projection against app-server config

This commit is contained in:
murashit 2026-05-26 17:00:56 +09:00
parent bde7b65974
commit f000fa7e75
2 changed files with 60 additions and 37 deletions

View file

@ -1,73 +1,86 @@
import type { ReasoningEffort } from "../generated/app-server/ReasoningEffort";
import type { ReasoningSummary } from "../generated/app-server/ReasoningSummary";
import type { Verbosity } from "../generated/app-server/Verbosity";
import type { WebSearchMode } from "../generated/app-server/WebSearchMode";
import type { ConfigReadResponse } from "../generated/app-server/v2/ConfigReadResponse";
import type { AppsConfig } from "../generated/app-server/v2/AppsConfig";
import type { AskForApproval } from "../generated/app-server/v2/AskForApproval";
import type { ApprovalsReviewer } from "../generated/app-server/v2/ApprovalsReviewer";
import type { Config } from "../generated/app-server/v2/Config";
import type { ProfileV2 } from "../generated/app-server/v2/ProfileV2";
import type { SandboxMode } from "../generated/app-server/v2/SandboxMode";
import type { SandboxWorkspaceWrite } from "../generated/app-server/v2/SandboxWorkspaceWrite";
import type { ToolsV2 } from "../generated/app-server/v2/ToolsV2";
import { parseServiceTier, type ServiceTier } from "../app-server/service-tier";
import { isReasoningEffort } from "./model";
export interface RuntimeConfigProjection {
profile: string | null;
model: string | null;
modelProvider: unknown;
modelProvider: string | null;
reasoningEffort: ReasoningEffort | null;
rawReasoningEffort: string | null;
reasoningSummary: unknown;
verbosity: unknown;
reasoningSummary: ReasoningSummary | null;
verbosity: Verbosity | null;
serviceTier: ServiceTier | null;
approvalsReviewer: ApprovalsReviewer | null;
approvalPolicy: unknown;
webSearch: unknown;
approvalPolicy: AskForApproval | null;
webSearch: WebSearchMode | null;
modelContextWindow: number | null;
autoCompactTokenLimit: number | null;
sandboxMode: unknown;
workspaceNetworkAccess: unknown;
writableRoots: unknown;
sandboxMode: SandboxMode | null;
workspaceNetworkAccess: SandboxWorkspaceWrite["network_access"] | null;
writableRoots: SandboxWorkspaceWrite["writable_roots"] | null;
hooksEnabled: unknown;
applyPatchFreeformEnabled: unknown;
toolWebSearch: unknown;
apps: Record<string, unknown>;
toolWebSearch: ToolsV2["web_search"] | null;
apps: AppsConfig | null;
}
export function readRuntimeConfig(effectiveConfig: ConfigReadResponse | null): RuntimeConfigProjection {
const config = configRecord(effectiveConfig);
const features = asRecord(config["features"]);
const tools = asRecord(resolvedConfigValue(config, "tools"));
const workspaceWrite = asRecord(config["sandbox_workspace_write"]);
const tools = resolvedConfigValue(config, "tools") ?? null;
const workspaceWrite = config.sandbox_workspace_write ?? null;
const effort = resolvedConfigValue(config, "model_reasoning_effort");
return {
profile: stringOrNull(config["profile"]),
profile: stringOrNull(config.profile),
model: nonEmptyStringOrNull(resolvedConfigValue(config, "model")),
modelProvider: resolvedConfigValue(config, "model_provider"),
modelProvider: resolvedConfigValue(config, "model_provider") ?? null,
reasoningEffort: isReasoningEffort(effort) ? effort : null,
rawReasoningEffort: nonEmptyStringOrNull(effort),
reasoningSummary: resolvedConfigValue(config, "model_reasoning_summary"),
verbosity: resolvedConfigValue(config, "model_verbosity"),
reasoningSummary: resolvedConfigValue(config, "model_reasoning_summary") ?? null,
verbosity: resolvedConfigValue(config, "model_verbosity") ?? null,
serviceTier: parseServiceTier(resolvedConfigValue(config, "service_tier")),
approvalsReviewer: approvalsReviewerOrNull(resolvedConfigValue(config, "approvals_reviewer")),
approvalPolicy: resolvedConfigValue(config, "approval_policy"),
webSearch: resolvedConfigValue(config, "web_search"),
modelContextWindow: numberOrNull(config["model_context_window"]),
autoCompactTokenLimit: numberOrNull(config["model_auto_compact_token_limit"]),
sandboxMode: config["sandbox_mode"],
workspaceNetworkAccess: workspaceWrite["network_access"],
writableRoots: workspaceWrite["writable_roots"],
approvalPolicy: resolvedConfigValue(config, "approval_policy") ?? null,
webSearch: resolvedConfigValue(config, "web_search") ?? null,
modelContextWindow: numberOrNull(config.model_context_window),
autoCompactTokenLimit: numberOrNull(config.model_auto_compact_token_limit),
sandboxMode: config.sandbox_mode ?? null,
workspaceNetworkAccess: workspaceWrite?.network_access ?? null,
writableRoots: workspaceWrite?.writable_roots ?? null,
hooksEnabled: features["hooks"],
applyPatchFreeformEnabled: features["apply_patch_freeform"],
toolWebSearch: tools["web_search"],
apps: asRecord(config["apps"]),
toolWebSearch: tools?.web_search ?? null,
apps: config.apps ?? null,
};
}
function configRecord(effectiveConfig: ConfigReadResponse | null): Record<string, unknown> {
return fillMissingConfigValues(asRecord(effectiveConfig?.config), effectiveConfig?.layers ?? null);
type ConfigProjectionRecord = Partial<Config> & Record<string, unknown>;
type ProfileProjectionRecord = Partial<ProfileV2> & Record<string, unknown>;
function configRecord(effectiveConfig: ConfigReadResponse | null): ConfigProjectionRecord {
return fillMissingConfigValues(asConfigRecord(effectiveConfig?.config), effectiveConfig?.layers ?? null);
}
function selectedProfileConfig(config: Record<string, unknown>): Record<string, unknown> {
const profile = config["profile"];
function selectedProfileConfig(config: ConfigProjectionRecord): ProfileProjectionRecord {
const profile = config.profile;
const profileName = typeof profile === "string" && profile.length > 0 ? profile : null;
return profileName ? asRecord(asRecord(config["profiles"])[profileName]) : {};
return profileName ? asProfileRecord(asRecord(config.profiles)[profileName]) : {};
}
function resolvedConfigValue(config: Record<string, unknown>, key: string): unknown {
function resolvedConfigValue<K extends keyof Config>(config: ConfigProjectionRecord, key: K): Config[K] | undefined {
const profileValue = selectedProfileConfig(config)[key];
return profileValue ?? config[key];
}
@ -80,14 +93,23 @@ function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function fillMissingConfigValues(config: Record<string, unknown>, layers: ConfigReadResponse["layers"]): Record<string, unknown> {
function asConfigRecord(value: unknown): ConfigProjectionRecord {
return asRecord(value) as ConfigProjectionRecord;
}
function asProfileRecord(value: unknown): ProfileProjectionRecord {
return asRecord(value) as ProfileProjectionRecord;
}
function fillMissingConfigValues(config: ConfigProjectionRecord, layers: ConfigReadResponse["layers"]): ConfigProjectionRecord {
if (!layers || layers.length === 0) return config;
const fallback = rawLayerConfigRecord(layers);
if (Object.keys(fallback).length === 0) return config;
const merged = { ...config };
const merged: ConfigProjectionRecord = { ...config };
const mergedRecord = merged as Record<string, unknown>;
for (const [key, value] of Object.entries(fallback)) {
merged[key] ??= value;
mergedRecord[key] ??= value;
}
return merged;
}

View file

@ -262,13 +262,14 @@ function writableRootsLabel(value: unknown): string {
return `${String(value.length)} roots`;
}
function enabledAppsLabel(apps: Record<string, unknown>): string {
const enabled = Object.entries(apps)
function enabledAppsLabel(apps: unknown): string {
const appConfig = asRecord(apps);
const enabled = Object.entries(appConfig)
.filter(([key, app]) => key !== "_default" && asRecord(app)["enabled"] === true)
.map(([key]) => key)
.sort();
if (enabled.length > 0) return enabled.join(", ");
const defaultConfig = asRecord(apps["_default"]);
const defaultConfig = asRecord(appConfig["_default"]);
return stringValue(defaultConfig["enabled"], "(from default)");
}