repositories / will
will
owned by admin
src/agent/config.ts
Rawimport { join } from 'node:path'
import { type BudgetConfig, DEFAULT_BUDGET } from '../shared/budget.ts'
import { envInt, envOpt, envStr } from '../shared/env.ts'
/**
* Agent runtime configuration (spec: daily budget lives in deployment
* configuration outside the mutable repository; env with sane defaults here).
*/
export interface AgentConfig {
agentDir: string
workDir: string
resourceDir: string
sessionFile: string
serviceUrl: string
readyAddr: string
telegram: {
token?: string | undefined
chatId?: number | undefined
oliverId?: string | undefined
apiBase?: string | undefined
}
vcs: { gitUrl?: string | undefined; branch: string }
zai: {
apiKey?: string | undefined
baseUrl: string
/** multimodal coding model used by zai_analyze_media and the resident session (pinned deployment config) */
analysisModel: string
/** resident session thinking level; Pi default 'medium' made explicit (pinned deployment config) */
thinkingLevel: ThinkingLevel
mcp: { enabled: boolean; serverCommand: string[] }
}
deployApiUrl?: string | undefined
luci: { baseUrl?: string | undefined; repo?: string | undefined }
budget: BudgetConfig
revision?: string | undefined
agentDigest?: string | undefined
maxTurnMinutes: number
}
export type SessionMode = 'real' | 'fake'
/** Pi thinking levels; the model clamps unsupported ones (see thinkingLevelMap) */
export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'
const THINKING_LEVELS: readonly ThinkingLevel[] = [
'off',
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
]
function thinkingLevelFromEnv(env: NodeJS.ProcessEnv): ThinkingLevel {
const raw = env.ZAI_THINKING_LEVEL ?? 'medium'
if (!(THINKING_LEVELS as readonly string[]).includes(raw)) {
throw new Error(`ZAI_THINKING_LEVEL must be one of ${THINKING_LEVELS.join('|')}; got '${raw}'`)
}
return raw as ThinkingLevel
}
/**
* Session selection: explicit fake wins; explicit real demands a key;
* unset falls back by key presence so `ZAI_API_KEY=... mise run dev`
* gives the real session with zero extra flags (spec: fake is dev-only).
*/
export function resolveSessionMode(env: NodeJS.ProcessEnv = process.env): SessionMode {
const explicit = env.WILL_SESSION_MODE
if (explicit === 'fake') return 'fake'
if (explicit === 'real') {
if (!env.ZAI_API_KEY) throw new Error('WILL_SESSION_MODE=real requires ZAI_API_KEY')
return 'real'
}
return env.ZAI_API_KEY ? 'real' : 'fake'
}
export function loadAgentConfig(env: NodeJS.ProcessEnv = process.env): AgentConfig {
const agentDir = envStr('WILL_AGENT_DIR', '/srv/will-agent')
const resourceDir = envStr('WILL_RESOURCE_DIR', '/app/repo')
const budgetFileCap = env.WILL_BUDGET_DAILY_CAP
const budgetFraction = env.WILL_BUDGET_RECOVERY_FRACTION
const tgChat = env.WILL_TELEGRAM_CHAT_ID
return {
agentDir,
workDir: envOpt('WILL_WORK_DIR') ?? join(agentDir, 'work'),
resourceDir,
sessionFile: join(agentDir, 'session.jsonl'),
serviceUrl: envStr('WILL_SERVICE_INTERNAL_URL', 'http://127.0.0.1:3001'),
readyAddr: envStr('WILL_AGENT_READY_ADDR', '127.0.0.1:8081'),
telegram: {
token: envOpt('TELEGRAM_BOT_TOKEN') ?? envOpt('WILL_TELEGRAM_TOKEN'),
chatId: tgChat !== undefined && tgChat !== '' ? Number.parseInt(tgChat, 10) : undefined,
oliverId: envOpt('WILL_TELEGRAM_OLIVER_ID'),
apiBase: envOpt('WILL_TELEGRAM_API_BASE'),
},
vcs: {
gitUrl: envOpt('WILL_VCS_GIT_URL'),
branch: envStr('WILL_VCS_DEFAULT_BRANCH', 'trunk'),
},
zai: {
apiKey: envOpt('ZAI_API_KEY'),
baseUrl: envStr('ZAI_BASE_URL', 'https://api.z.ai/api'),
analysisModel: envStr('ZAI_ANALYSIS_MODEL', 'glm-5.3-flash'),
thinkingLevel: thinkingLevelFromEnv(env),
mcp: {
enabled: env.WILL_ZAI_MCP === '1',
serverCommand: [
'node',
envStr(
'WILL_ZAI_MCP_SERVER',
join('node_modules', '@z_ai', 'mcp-server', 'build', 'index.js'),
),
],
},
},
deployApiUrl: envOpt('WILL_DEPLOY_API_URL'),
luci: { baseUrl: envOpt('WILL_LUCI_BASE_URL'), repo: envOpt('WILL_LUCI_REPO') },
budget: {
dailyCapCalls:
budgetFileCap !== undefined && budgetFileCap !== ''
? Number.parseInt(budgetFileCap, 10)
: DEFAULT_BUDGET.dailyCapCalls,
recoveryReserveFraction:
budgetFraction !== undefined && budgetFraction !== ''
? Number.parseFloat(budgetFraction)
: DEFAULT_BUDGET.recoveryReserveFraction,
},
revision: envOpt('WILL_REVISION'),
agentDigest: envOpt('WILL_AGENT_DIGEST'),
maxTurnMinutes: envInt('WILL_TURN_DEADLINE_MINUTES', 30),
}
}