Config split: agent.json (connection) + config.json (LLM)

- loadAgentConfig() reads both files with priority: env > config.json > agent.json
- Remote config merge from auth.ok (model, provider, prompt)
- Local config.json overrides remote values
- Transport default changed to 'ws'
This commit is contained in:
Markov 2026-02-27 16:30:37 +01:00
parent 1c7fdf8d77
commit 6871fdb443
3 changed files with 61 additions and 8 deletions

View File

@ -116,7 +116,7 @@ export function hasAgentConfig(): boolean {
export function loadAgentConfig(): AgentConfig { export function loadAgentConfig(): AgentConfig {
const { configPath, agentHome } = resolveConfig(); const { configPath, agentHome } = resolveConfig();
// Load file if available, otherwise empty object // Load agent.json (connection config)
let file: Record<string, unknown> = {}; let file: Record<string, unknown> = {};
if (configPath) { if (configPath) {
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
@ -125,6 +125,17 @@ export function loadAgentConfig(): AgentConfig {
file = JSON.parse(fs.readFileSync(configPath, 'utf-8')); file = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
} }
// Load config.json (LLM config) — separate file, optional
let llm: Record<string, unknown> = {};
const llmConfigPath = configPath
? path.join(path.dirname(configPath), 'config.json')
: agentHome
? path.join(agentHome, 'config.json')
: null;
if (llmConfigPath && fs.existsSync(llmConfigPath)) {
llm = JSON.parse(fs.readFileSync(llmConfigPath, 'utf-8'));
}
const trackerUrl = process.env.TRACKER_URL || (file.tracker_url as string) || ''; const trackerUrl = process.env.TRACKER_URL || (file.tracker_url as string) || '';
const token = process.env.AGENT_TOKEN || (file.token as string) || ''; const token = process.env.AGENT_TOKEN || (file.token as string) || '';
@ -144,22 +155,23 @@ export function loadAgentConfig(): AgentConfig {
const wsUrl = process.env.AGENT_WS_URL || (file.ws_url as string) || ''; const wsUrl = process.env.AGENT_WS_URL || (file.ws_url as string) || '';
// Priority: env > config.json (LLM) > agent.json (legacy compat) > defaults
return { return {
name: (file.name as string) || process.env.AGENT_NAME || 'Agent', name: (file.name as string) || process.env.AGENT_NAME || 'Agent',
slug: (file.slug as string) || process.env.AGENT_SLUG || 'agent', slug: (file.slug as string) || process.env.AGENT_SLUG || 'agent',
prompt: (file.prompt as string) || process.env.AGENT_PROMPT || '', prompt: (llm.prompt as string) || (file.prompt as string) || process.env.AGENT_PROMPT || '',
trackerUrl, trackerUrl,
wsUrl, wsUrl,
token, token,
transport: (process.env.AGENT_TRANSPORT || (file.transport as string) || 'http') as 'http' | 'ws', transport: (process.env.AGENT_TRANSPORT || (file.transport as string) || 'ws') as 'http' | 'ws',
listenPort: parseInt(process.env.AGENT_PORT || String(file.listen_port || '3200'), 10), listenPort: parseInt(process.env.AGENT_PORT || String(file.listen_port || '3200'), 10),
workDir, workDir,
agentHome: resolvedHome || workDir, agentHome: resolvedHome || workDir,
capabilities: (file.capabilities as string[]) || ['coding'], capabilities: (file.capabilities as string[]) || ['coding'],
maxConcurrentTasks: (file.max_concurrent_tasks as number) || 2, maxConcurrentTasks: (llm.max_concurrent_tasks as number) || (file.max_concurrent_tasks as number) || 2,
model: process.env.PICOGENT_MODEL || (file.model as string) || 'sonnet', model: process.env.PICOGENT_MODEL || (llm.model as string) || (file.model as string) || 'sonnet',
provider: process.env.PICOGENT_PROVIDER || (file.provider as string) || 'anthropic', provider: process.env.PICOGENT_PROVIDER || (llm.provider as string) || (file.provider as string) || 'anthropic',
apiKey: process.env.PICOGENT_API_KEY || process.env.ANTHROPIC_API_KEY || (file.api_key as string) || '', apiKey: process.env.PICOGENT_API_KEY || process.env.ANTHROPIC_API_KEY || (llm.api_key as string) || (file.api_key as string) || '',
heartbeatIntervalSec: (file.heartbeat_interval_sec as number) || 30, heartbeatIntervalSec: (file.heartbeat_interval_sec as number) || 30,
allowedPaths: (file.allowed_paths as string[]) || [], allowedPaths: (file.allowed_paths as string[]) || [],
sessionId: ensureSessionId(file, configPath), sessionId: ensureSessionId(file, configPath),

View File

@ -51,6 +51,25 @@ async function startAgentWs(config: AgentConfig, client: TrackerClient): Promise
router.setProjectMappings(wsTransport.projects); router.setProjectMappings(wsTransport.projects);
} }
// Merge remote config (Tracker is source of truth, local config.json overrides)
if (wsTransport.remoteConfig) {
const rc = wsTransport.remoteConfig;
// Only apply remote values if local config.json didn't set them
// (loadAgentConfig already prioritized config.json > agent.json)
if (rc.model && !process.env.PICOGENT_MODEL && config.model === 'sonnet') {
config.model = rc.model;
logger.info('Applied remote model: %s', rc.model);
}
if (rc.provider && !process.env.PICOGENT_PROVIDER && config.provider === 'anthropic') {
config.provider = rc.provider;
logger.info('Applied remote provider: %s', rc.provider);
}
if (rc.prompt && !config.prompt) {
config.prompt = rc.prompt;
logger.info('Applied remote prompt (%d chars)', rc.prompt.length);
}
}
const shutdown = () => { const shutdown = () => {
logger.info('Shutting down agent (ws)...'); logger.info('Shutting down agent (ws)...');
wsTransport.stop().then(() => { wsTransport.stop().then(() => {

View File

@ -36,6 +36,14 @@ export class WsClientTransport implements TaskTracker {
projects: Array<{ id: string; slug: string; name: string; chat_id?: string }> = []; projects: Array<{ id: string; slug: string; name: string; chat_id?: string }> = [];
/** Online members from auth.ok */ /** Online members from auth.ok */
online: string[] = []; online: string[] = [];
/** Remote agent config from auth.ok (if agent) */
remoteConfig: {
model?: string;
provider?: string;
prompt?: string;
max_concurrent_tasks?: number;
capabilities?: string[];
} | null = null;
constructor(private config: AgentConfig) {} constructor(private config: AgentConfig) {}
@ -205,11 +213,25 @@ export class WsClientTransport implements TaskTracker {
this.projects = (data.projects as Array<{ id: string; slug: string; name: string; chat_id?: string }>) || []; this.projects = (data.projects as Array<{ id: string; slug: string; name: string; chat_id?: string }>) || [];
this.online = (data.online as string[]) || (data.agents_online as string[]) || []; this.online = (data.online as string[]) || (data.agents_online as string[]) || [];
// Remote agent config (if present)
const agentConfig = data.agent_config as Record<string, unknown> | undefined;
if (agentConfig) {
this.remoteConfig = {
model: agentConfig.model as string | undefined,
provider: agentConfig.provider as string | undefined,
prompt: agentConfig.prompt as string | undefined,
max_concurrent_tasks: agentConfig.max_concurrent_tasks as number | undefined,
capabilities: agentConfig.capabilities as string[] | undefined,
};
}
this.log.info('━━━ AUTH OK ━━━'); this.log.info('━━━ AUTH OK ━━━');
this.log.info(' Lobby chat: %s', this.lobbyChatId || '(none)'); this.log.info(' Lobby chat: %s', this.lobbyChatId || '(none)');
this.log.info(' Projects: %s', this.projects.map(p => `${p.slug}(${p.id})`).join(', ') || '(none)'); this.log.info(' Projects: %s', this.projects.map(p => `${p.slug}(${p.id})`).join(', ') || '(none)');
this.log.info(' Online: %s', this.online.join(', ') || '(nobody)'); this.log.info(' Online: %s', this.online.join(', ') || '(nobody)');
this.log.info(' Full auth data: %s', JSON.stringify(data, null, 2)); if (this.remoteConfig) {
this.log.info(' Remote config: model=%s provider=%s', this.remoteConfig.model || '-', this.remoteConfig.provider || '-');
}
this.authenticated = true; this.authenticated = true;
this.reconnectDelay = 1000; this.reconnectDelay = 1000;