Volver al índice
src/main/java/ar/com/companeros/runtime/AgentConfig.java
package ar.com.companeros.runtime;
import ar.com.companeros.decision.DecisionGateway;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.Set;
/** Configuración del operador; nunca contiene una clave de API o datos enviados al modelo. */
public final class AgentConfig {
// Defaults de ensayo: dos jugadores y ninguna llamada paga automática.
private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create();
private static final Set<String> FIELDS = Set.of("schemaVersion", "populationLimit", "modelsEnabled",
"foundationVerified", "solEnabled", "maxSessionUsd", "minAgentIntervalSeconds", "maxRequestsHour", "autoConnect", "webEnabled");
private final Path file;
private int populationLimit = 2;
private boolean modelsEnabled, foundationVerified, solEnabled;
private BigDecimal maxSessionUsd = BigDecimal.ZERO;
private int minAgentIntervalSeconds = 60, maxRequestsHour = 30;
private boolean autoConnect = true;
private boolean webEnabled;
private ResearchConfig research = ResearchConfig.defaults();
private AgentConfig(Path file) { this.file = file; }
public static AgentConfig load(Path configDir) {
Path root = configDir.toAbsolutePath().normalize();
Path legacy = root.resolve("companeros"), current = root.resolve("raps");
try { if (Files.isDirectory(legacy) && !Files.exists(current)) Files.move(legacy, current); }
catch (IOException failure) { throw new IllegalStateException("No se pudo migrar la configuración anterior", failure); }
Path file = current.resolve("agents.json");
AgentConfig config = new AgentConfig(file);
try {
config.research = ResearchConfig.load(current);
config.webEnabled = config.research.enabled();
if (!Files.exists(file)) { config.write(); return config; }
if (Files.size(file) > 16_384) throw new IOException("Configuración fuera de límite");
JsonObject data = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).getAsJsonObject();
if (!FIELDS.containsAll(data.keySet()) || integer(data, "schemaVersion", 1) != 1)
throw new IllegalArgumentException("Formato de configuración no reconocido");
config.populationLimit = integer(data, "populationLimit", 2); validPopulation(config.populationLimit);
config.modelsEnabled = bool(data, "modelsEnabled", false);
config.foundationVerified = bool(data, "foundationVerified", false);
config.solEnabled = bool(data, "solEnabled", false);
config.autoConnect = bool(data, "autoConnect", true);
config.maxSessionUsd = data.has("maxSessionUsd") ? data.get("maxSessionUsd").getAsBigDecimal() : BigDecimal.ZERO;
config.minAgentIntervalSeconds = integer(data, "minAgentIntervalSeconds", 60);
config.maxRequestsHour = integer(data, "maxRequestsHour", 30);
config.decisionConfig(); // Valida cupos antes de habilitar cualquier proveedor.
return config;
} catch (IOException | RuntimeException error) {
throw new IllegalStateException("No se pudo cargar agents.json; se conserva el archivo original", error);
}
}
// Lectura y modificación limitada de población; flags del proveedor se administran en archivo.
public synchronized int populationLimit() { return populationLimit; }
public synchronized boolean autoConnect() { return autoConnect; }
public synchronized boolean webEnabled() { return webEnabled; }
public synchronized void setPopulationLimit(int value) {
validPopulation(value); int previous = populationLimit; populationLimit = value;
try { write(); }
catch (IOException error) {
populationLimit = previous;
throw new IllegalStateException("No se pudo guardar el límite de población", error);
}
}
public synchronized DecisionGateway.Config decisionConfig() {
return new DecisionGateway.Config(modelsEnabled, foundationVerified, solEnabled, maxSessionUsd,
maxRequestsHour, 16, 2, 2000, minAgentIntervalSeconds, Duration.ofSeconds(20), webEnabled,
research.maxRequestsHour(), research.minAgentIntervalSeconds(), research.allowedDomains());
}
// Guardado atómico: rutas fijas del servicio confiable, sin parámetros de filesystem para bots.
private void write() throws IOException {
Files.createDirectories(file.getParent());
JsonObject data = new JsonObject(); data.addProperty("schemaVersion", 1);
data.addProperty("populationLimit", populationLimit); data.addProperty("modelsEnabled", modelsEnabled);
data.addProperty("foundationVerified", foundationVerified); data.addProperty("solEnabled", solEnabled);
data.addProperty("maxSessionUsd", maxSessionUsd); data.addProperty("minAgentIntervalSeconds", minAgentIntervalSeconds);
data.addProperty("maxRequestsHour", maxRequestsHour); data.addProperty("autoConnect", autoConnect);
Path temporary = file.resolveSibling("agents.json.tmp");
Files.writeString(temporary, JSON.toJson(data) + System.lineSeparator(), StandardCharsets.UTF_8);
try { Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); }
catch (AtomicMoveNotSupportedException unavailable) { Files.move(temporary, file, StandardCopyOption.REPLACE_EXISTING); }
finally { Files.deleteIfExists(temporary); }
}
private static void validPopulation(int value) {
if (value != 2 && value != 4 && value != 6 && value != 10)
throw new IllegalArgumentException("La población admite 2, 4, 6 o 10 jugadores");
}
private static int integer(JsonObject data, String key, int fallback) {
if (!data.has(key)) return fallback;
if (!data.get(key).isJsonPrimitive() || !data.get(key).getAsJsonPrimitive().isNumber())
throw new IllegalArgumentException("Valor numérico inválido");
return data.get(key).getAsBigDecimal().intValueExact();
}
private static boolean bool(JsonObject data, String key, boolean fallback) {
if (!data.has(key)) return fallback;
if (!data.get(key).isJsonPrimitive() || !data.get(key).getAsJsonPrimitive().isBoolean())
throw new IllegalArgumentException("Valor booleano inválido");
return data.get(key).getAsBoolean();
}
}