AkurAI Build
Menu

popagent

public

Latest change 6726bbcb1e8a68b297a0e59825c4d913d4b9aab8 - Run autonomous improvements continuously by Ólafur Búi Ólafsson

import { Cron } from "croner";
import type { AutonomySettings, AutonomySettingsInput } from "./api-types";
import { initializeAgentRoleStorage } from "./agent-settings";
import { retryableInit } from "./retryable-init";
import { storage } from "./storage";

const AUTONOMY_SETTINGS_COLUMNS = `
  enabled,
  reflection_interval_ms AS "reflectionIntervalMs",
  batch_size AS "batchSize",
  max_attempts AS "maxAttempts",
  auto_apply_strategies AS "autoApplyStrategies",
  auto_create_skills AS "autoCreateSkills",
  self_update_enabled AS "selfUpdateEnabled",
  self_update_cron AS "selfUpdateCron",
  idle_improvement_enabled AS "idleImprovementEnabled",
  idle_deployment_enabled AS "idleDeploymentEnabled",
  idle_workspace_ids AS "idleWorkspaceIds",
  updated_at::text AS "updatedAt"
`;

const initializeEvolutionStorageOnce = retryableInit(async () => {
  await initializeAgentRoleStorage();
  const schema = await Bun.file(new URL("./evolution.sql", import.meta.url)).text();
  await storage.db.none(schema);
});

export function initializeEvolutionStorage(): Promise<void> {
  return initializeEvolutionStorageOnce();
}

function validate(input: AutonomySettingsInput): AutonomySettingsInput {
  if (!Number.isInteger(input.reflectionIntervalMs)
    || input.reflectionIntervalMs < 60_000
    || input.reflectionIntervalMs > 86_400_000) {
    throw new RangeError("Reflection interval must be between 60000 and 86400000 milliseconds");
  }
  if (!Number.isInteger(input.batchSize) || input.batchSize < 1 || input.batchSize > 100) {
    throw new RangeError("Evolution batch size must be between 1 and 100");
  }
  if (!Number.isInteger(input.maxAttempts) || input.maxAttempts < 1 || input.maxAttempts > 10) {
    throw new RangeError("Evolution max attempts must be between 1 and 10");
  }
  if (new Set(input.idleWorkspaceIds).size !== input.idleWorkspaceIds.length
    || input.idleWorkspaceIds.some((id) => !id.trim() || id.length > 256)) {
    throw new RangeError("Idle workspace IDs must be unique non-empty values");
  }
  const selfUpdateCron = input.selfUpdateCron.trim();
  if (!selfUpdateCron || selfUpdateCron.length > 100) {
    throw new RangeError("Self-update cron must be between 1 and 100 characters");
  }
  try {
    new Cron(selfUpdateCron, { paused: true }).nextRun();
  } catch {
    throw new RangeError("Self-update cron is invalid");
  }
  return { ...input, selfUpdateCron };
}

export class AutonomySettingsStore {
  readonly storage = storage;
  private readonly listeners = new Set<(settings: AutonomySettings) => void>();

  init(): Promise<void> {
    return initializeEvolutionStorage();
  }

  async get(): Promise<AutonomySettings> {
    await this.init();
    return this.storage.db.one<AutonomySettings>(`
      SELECT ${AUTONOMY_SETTINGS_COLUMNS}
      FROM popagent_autonomy_settings
      WHERE singleton = TRUE
    `);
  }

  async update(rawInput: AutonomySettingsInput): Promise<AutonomySettings> {
    const input = validate(rawInput);
    await this.init();
    const settings = await this.storage.db.one<AutonomySettings>(`
      UPDATE popagent_autonomy_settings
      SET enabled = $1,
          reflection_interval_ms = $2,
          batch_size = $3,
          max_attempts = $4,
          auto_apply_strategies = $5,
          auto_create_skills = $6,
          self_update_enabled = $7,
          self_update_cron = $8,
          idle_improvement_enabled = $9,
          idle_deployment_enabled = $10,
          idle_workspace_ids = $11::jsonb,
          updated_at = NOW()
      WHERE singleton = TRUE
      RETURNING ${AUTONOMY_SETTINGS_COLUMNS}
    `, [
      input.enabled,
      input.reflectionIntervalMs,
      input.batchSize,
      input.maxAttempts,
      input.autoApplyStrategies,
      input.autoCreateSkills,
      input.selfUpdateEnabled,
      input.selfUpdateCron,
      input.idleImprovementEnabled,
      input.idleDeploymentEnabled,
      JSON.stringify(input.idleWorkspaceIds),
    ]);
    for (const listener of this.listeners) listener(settings);
    return settings;
  }

  subscribe(listener: (settings: AutonomySettings) => void): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}

export const autonomySettings = new AutonomySettingsStore();