← Vissza a bloghoz
goalkomplexitásmemóriaarchitektúra

A Codumentor Goal-rendszere: autonóm, hosszú élettartamú feladatok a komplexitás költségei nélkül

·ARDINSYS Codumentor

Amikor nekiálltunk, hogy hosszú élettartamú, autonóm feladatokat építsünk be a Codumentorba, egy elterjedt feltételezést örököltünk: a goal-oknak dedikált futtatókörnyezetre van szükségük. Az ember egy GoalManager singletonra számítana, egy egyedi eseményhurokra, esetleg egy üzenetbrókerre — a szokásos infrastruktúra-költségre minden olyan dologért, ami túléli az egyetlen kérést.

Amit végül megépítettünk, az alapvetően más. A Codumentor goal-jai ütemezett beszélgetések gyűjteményei, amelyeket egy közös fájlrendszer koordinál. Nincs egyedi futtatókörnyezet, nincs üzenetsor, nincs tartósan futó folyamat. Csak cron, SQLite, és egy gondosan megtervezett könyvtárstruktúra, amely egyszerre szolgál állapotként és kommunikációs csatornaként.

Nézzük, hogyan működik mindez, miért ezeket a döntéseket hoztuk, és milyen kompromisszumokat vállaltunk közben.

A probléma: feladatok, amelyek túlélik a beszélgetéseket

A csevegésalapú AI-asszisztensek a természetükből fakadóan efemerek. Elindul egy beszélgetés, elvégződik valamennyi munka, majd a kontextus eltűnik. Sok fejlesztői feladat azonban nem hajlandó beilleszkedni ebbe a modellbe:

  • „Tartsuk szinkronban a kódbázis dokumentációját a valósággal”
  • „Vizsgáljunk át hetente minden PR-t dokumentációs hiányosságok után kutatva”
  • „Írjunk technikai blogbejegyzés-vázlatot minden kedden és csütörtökön”

Ezek nem egyszeri kérések. Folyamatos szándékok, amelyeknek napokon, heteken és hónapokon át kell fennmaradniuk — állapotot tartva a futások között, több specializált workert koordinálva, és a hibákból elegánsan felépülve.

„Goal”-oknak neveztük el őket, mert pontosan azok: hosszú távú célok, amelyeket egy autonóm ügynök fokozatosan valósít meg, nem pedig egyszeri feladatok.

A központi felismerés: a tudásbázis mint az igazság forrása

Az az architekturális döntés, amely mindent mást is meghatározott, egy egyszerű felismerésből fakadt:

A tudásbázis az igazság forrása. Az ütemező az ébresztőóra; a tudásbázis a feladatleírás.

Minden worker minden egyes tick alkalmával ugyanazt a statikus promptot kapja — egy vékony „shim”-et, amely a lemezen tárolt valódi utasításaira mutat:

def build_shim_prompt(*, kb_path: str, worker_name: str, goal_id: str) -> str:
    return (
        f"You are worker `{worker_name}` for goal `{goal_id}`.\n\n"
        f"Your knowledge base is at: `{kb_path}`\n\n"
        f"Read these two files first, in order, then proceed:\n"
        f"  1. `{kb_path}/workers/{worker_name}/prompt.md` "
        f"(your role + instructions)\n"
        f"  2. `{kb_path}/protocol.md` (the goal's protocol)\n\n"
        f"Then act according to those documents. Use the file tools available "
        f"in your environment to read/write inside the KB."
    )

Ez a prompt soha nem változik. De azok a fájlok, amelyekre mutat — a prompt.md és a protocol.md — közönséges fájlok a lemezen. Szerkeszd őket, és a következő tick automatikusan felveszi a változásokat. Nincs újraütemezés, nincs újratelepítés, nincs API-hívás.

Egy ember megnyithatja egy goal könyvtárát a szerkesztőjében, módosíthatja egy worker utasításait, és az autonóm rendszer a következő ütemezett futásán alkalmazkodik hozzá. A tudásbázis nem adatbázis; egy közös jegyzetfüzet, amelyet a workerek olvasnak és írnak.

A lemezen tárolt struktúra: a közös jegyzetfüzeted

Minden goal egy ilyen könyvtárfát kap:

<repo_root>/<storage_path>/<goal_id>/
├── definition.yaml          # Azonosító adatok: goal_id, name, task, owner, status
├── protocol.md              # Szerződés: üzenetformátumok, konvenciók, KPI-k
├── state.yaml               # Korlátozott, tickeken átívelő állapot: tervek, döntések
├── log/
│   └── YYYY-MM-DD.md        # Csak hozzáfűzhető eseménynapló
└── workers/
    └── <worker_name>/
        ├── definition.yaml  # Cron-ütemezés, utasítások, képességek
        ├── prompt.md        # Legenerált worker-utasítások
        ├── inbox/           # Üzenetek a társ-workerektől
        └── outbox/          # Ennek a workernek az utolsó tickjéből származó eredmények

Ez szándékos. A goal állapotának minden egyes darabja egy fájl, amit cat-elhetsz, diff-elhetsz, és szerkeszthetsz a kedvenc szövegszerkesztőddel. Nincs saját bináris formátum, nincs adatbázis-dump.

Atomi könyvtárfa-létrehozás

Amikor egy goal létrejön, nem fájlonként írjuk meg a végleges könyvtárba — az ablakot nyitna, amelyben az olvasók egy félkész goal-t látnának. Ehelyett mindent egy ideiglenes könyvtárban építünk fel, majd átnevezzük a helyére:

def create_goal_tree(base_dir: Path, *, definition, workers, template_loader) -> Path:
    # Írjunk egy testvér tempdirbe, majd nevezzük át — atomi POSIX rendszereken
    staging_dir = Path(tempfile.mkdtemp(prefix=f".{definition.goal_id}.tmp-", dir=str(base_dir)))
    try:
        _write_yaml(staging_dir / "definition.yaml", definition.to_dict())
        _write_text(staging_dir / "protocol.md", render_protocol_seed(...))
        _write_yaml(staging_dir / "state.yaml", {"plan": [], "decisions": [], "baselines": {}})
        (staging_dir / "log").mkdir(...)
        for w in workers:
            # Hozzuk létre az inbox/-ot, outbox/-ot, definition.yaml-t, prompt.md-t
        os.rename(str(staging_dir), str(final_dir))  # Atomi POSIX rendszereken
    except Exception:
        shutil.rmtree(str(staging_dir), ignore_errors=True)
        raise

Az os.rename() atomi a POSIX rendszereken. Vagy a teljes goal-fa egyszerre jelenik meg, vagy semmi. Ezt kombinálva a minden-vagy-semmi ütemezés-regisztrációval — minden általunk létrehozott ütemezést visszagörgetünk, ha egy is meghiúsul —, a rendszer soha nem lát félig inicializált goal-t.

A workerek ütemezett beszélgetések, nem alügynökök

Itt válik igazán érdekessé az architektúra. A Codumentorban egy „worker” nem háttérszál vagy a fő ügynök által indított alügynök. Legfelső szintű, ütemezett beszélgetés — ugyanolyan típusú beszélgetés, amilyet a felhasználói felületen folytatnál a Codumentorral, csak épp cron indítja, nem egy felhasználó, aki beír egy üzenetet.

Minden tick egy vadonatúj beszélgetést hoz létre. Nincs megosztott, memóriában tárolt állapot a futások között — egyedül a tudásbázis marad meg. Ennek jelentős következményei vannak:

  • Nincsenek megosztott memóriából fakadó hibák. Két worker nem tudja megrongálni egymás állapotát, mert semmit nem osztanak meg.
  • Természetes izoláció. Minden worker a saját beszélgetési kontextusában fut, saját rendszerprompttal.
  • Az alügynök-képesség megmarad. A workerek továbbra is indíthatnak egy réteg alügynököt maguk alatt, tiszteletben tartva a meglévő „nincs rekurzív indítás” szabályt.
  • Egyszerű hibakeresés. Minden tick önálló beszélgetés, amit megvizsgálhatsz a felhasználói felületen.

Az ütemező: adaptív pollozás háttérszálak helyett

Az ezeket a beszélgetéseket indító ütemező nem szálanként-egy-ütemezés megközelítést használ. Ehelyett egy adaptív pollozó:

# Egyszerűsített ütemezőciklus
async def _loop():
    while True:
        now = time.time()
        due = await store.get_due(before_ts=now)

        for schedule in due:
            await _fire(schedule)  # Átfedés-ellenőrzés → léptetés, majd sorba állítás

        soonest = await store.get_soonest_next_fire()
        sleep_duration = compute_sleep(soonest)  # Adaptív pollozás
        await asyncio.sleep(sleep_duration)

Három mód szabályozza, hogy az ütemező mennyi ideig alszik:

  1. Egyáltalán nincs ütemezés → tétlen pollozási intervallum (30 másodperc)
  2. Már van esedékes elem → minimális pollozási intervallum (0,5 másodperc)
  3. A következő indítás a jövőben van → visszaszámlálás az adott időpontig, a tétlen intervallummal felülkorlátozva

Az új ütemezések egy eseménymechanizmuson keresztül korábban is felébreszthetik a pollozót. Ez egyszerűbb, mint több száz időzítőszálat fenntartani, és kevesebb memóriát használ.

A hibakezelés konzervatív

Az ütemező komolyan veszi a hibákat:

  • Exponenciális visszalépés (backoff): A meghiúsult ütemezések 2^n másodperc után próbálkoznak újra, legfeljebb 300 másodpercig.
  • Automatikus letiltás 5 egymást követő hiba után: Egy folyamatosan hibázó ütemezést nem éri meg örökké újrapróbálni.
  • Átfedés-védelem: Ha egy korábbi futás még végrehajtás alatt áll, az új indítás kimarad, és az ütemezés léptetve lesz.
  • 24 órás behozási korlát: Újraindításkor a 24 óránál régebb óta esedékes ütemezések teljesen kimaradnak.

Ezek nem önkényes korlátok — annak eredményei, hogy megfigyeltük, mi történik, amikor az autonóm rendszerek éles környezetben elromlanak.

Az eredeti terv vs. ami végül elkészült

A kezdeti tervünk sokkal monolitikusabb valamit vizionált: egyetlen GoalTool osztályt, amely alügynök-kezelőként viselkedik, a goal-okkal mint hosszan futó alügynök-példányokkal. A „tudásbázis-elsőbbség” koncepciója már megvolt, de egy egyedi futtatókörnyezetbe volt csomagolva.

Ami végül elkészült, az karcsúbb:

Eredeti terv Ami elkészült
Egyetlen GoalTool(AgenticTool) osztály Öt különálló, explicit eszköz
Goal-ok mint alügynök-példányok Goal-ok mint ütemezett feladatok gyűjteményei
Egyedi GoalManager futtatókörnyezet A meglévő ütemező-infrastruktúra újrafelhasználása
onServerReady helyreállítási hook SQLite-alapú perzisztencia, bepótlással újraindításkor

A terv és a megvalósítás közti eltérés nem kudarc — pragmatizmus. A meglévő scheduled_agent_run job-kezelő újrafelhasználásával elkerültük egy egyedi futtatókörnyezet megépítését és karbantartását. A kompromisszum az, hogy a workerek „butábbak” — fájlokat olvasnak ahelyett, hogy memóriában tárolt objektumokat kapnának —, de ez inkább jellemző, mint hiba. Ettől lesz a rendszer könnyebben hibakereshető és ellenállóbb.

Valós példa: a Nightshift-minta

A goal-ok által lehetővé tett dolgok legmeggyőzőbb bemutatója a tudásbázisunkban dokumentált Nightshift-minta:

  • Commander (20:00-kor indul): elemzi a kódbázist, azonosítja a fejlesztendő területeket, ütemezi a worker-feladatokat.
  • Workerek (20:30–07:00 között, eltolt indítással): mindegyik egy atomi feladatot hajt végre — egy hiba javítása, a dokumentáció frissítése, egy modul refaktorálása — majd elmenti az eredményeket a tudásbázisba.
  • Reporter (07:30-kor indul): beolvassa az összes worker kimenetét, és összeállít belőlük egy reggeli összefoglalót az emberi csapat számára.

A koordináció tudásbázis-dokumentumokon keresztül történik — egy közös jegyzetfüzeten, nem egy chatszobán. A workerek commitolnak branch-ekbe, de nem push-olnak; minden változtatást emberi jóváhagyás enged tovább. Ez a „hub-and-spoke raj” minta: autonóm ügynökök végeznek fókuszált munkát, amelyet egy közös fájlrendszer koordinál.

Biztonsági megfontolások: miért nincs hoszt-fájlrendszer-hozzáférés

Egy tervezési döntés, amely elsőre korlátozónak tűnhet: a goal-oknak kötelezően egy konfigurált repository-n belül kell élniük. Nincs megengedve hoszt-fájlrendszerre való visszaesés.

def resolve_base_dir(...) -> Path:
    # Feloldási sorrend:
    # 1. Explicit base_dir (csak teszteknél)
    # 2. Plugin argumentumként kapott storage_repo → config.repo_context.repo_root(name)
    # 3. Az agentic-memory plugin repo_name mezője
    # 4. Az egyrepós (single-repo) mód előtagja
    # 5. ValueError — nincs tartalék megoldás!

Ez nem kényelmi korlátozás — biztonsági határ. A worker sandbox a hoszt útvonalait a /workspace/... alá fordítja le. Egy repository-könyvtáron kívül eső goal-könyvtár elérhetetlen lenne a sandboxon belülről, ami eltérést okozna aközött, amit az ütemező létezőnek gondol, és amit a worker ténylegesen el tud érni.

Hasonlóképp, a goal-azonosítókat egy szigorú reguláris kifejezéssel validáljuk, amely elutasítja az útvonal-bejárási (path traversal) kísérleteket:

_SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$")

def make_goal_id(name: str) -> str:
    base = slugify(name)  # kisbetűs, kötőjellel elválasztott, max. 32 karakter
    return f"{base}-{uuid.uuid4().hex[:8]}"

A véletlenszerű utótag megakadályozza az ütközéseket, míg a reguláris kifejezés biztosítja, hogy se .., se /, se Windows-elválasztó ne kerülhessen be egy goal-azonosítóba.

Mi következik: az ütemterv

Az 1. fázis megadta az alapokat: goal-ok létrehozása, listázása, megtekintése, törlése, és kézi indítása. Az ütemterv azonban tovább nyúlik:

Fázis Állapot Képességek
1. fázis — Életciklus ✅ Megvalósítva setup_goal, list_goals, get_goal, delete_goal, run_worker_now
2. fázis — Tudásbázis-szerkesztés 🚧 Tervezett goal_kb_read, goal_kb_write, goal_kb_list, update_worker
2.5. fázis — Eszközkör szűkítése 📋 Megtervezve A worker eszközregisztereinek leszűkítése csak a szükséges eszközökre
3. fázis — Beállító ügynök + Orchestrator 🔮 Jövőbeli AI-asszisztált goal-létrehozás, dinamikus worker-koordináció
4. fázis — Láncolás / szétágazás 🔮 Jövőbeli Workerek, amelyek más workereket indítanak, párhuzamos végrehajtási minták

A 2. fázis különösen fontos: lehetővé teszi a workerek számára, hogy a tudásbázist megfelelő eszközökön keresztül olvassák és írják, nem pedig nyers fájlrendszer-hozzáférésre támaszkodva. Ez jobb auditálhatóságot, validációt, és idővel goal-ok közötti kommunikációt tesz lehetővé.

A tanulság

A Codumentor goal-rendszere bizonyítja, hogy kifinomult autonóm feladatkezelést lehet építeni anélkül, hogy kifinomult futtatókörnyezetet kellene építeni hozzá. Azzal, hogy a tudásbázist tekintettük az igazság forrásának, és újrafelhasználtuk a meglévő infrastruktúrát — a cron-ütemezést, az SQLite-ot, a fájlrendszert —, a következőket kaptuk:

  • Egyszerűség: nincs egyedi eseményhurok, nincs üzenetbróker, nincsenek tartósan futó folyamatok.
  • Hibakereshetőség: minden tick önálló beszélgetés; minden állapotváltozás egy fájl, amit diffelhetsz.
  • Ellenálló képesség: atomi írások, visszagörgetés hiba esetén, exponenciális visszalépés.
  • Bővíthetőség: szerkeszd egy worker prompt.md-jét, és a következő tick automatikusan alkalmazkodik.

A rendszer nem tökéletes — a memóriában tárolt koordináció hiánya azt jelenti, hogy a workerek lassan, fájlokon keresztül kommunikálnak, és a fájlalapú megközelítésnek teljesítménybeli következményei vannak a nagy gyakoriságú goal-oknál. De azokra a felhasználási esetekre, amelyekre terveztük — napi blogvázlat-írás, heti review-k, éjszakai fejlesztési műszakok —, az egyszerűség megbízhatóságban és karbantarthatóságban térül meg.

A tanulság az építők számára: ne építs futtatókörnyezetet ott, ahol egy jól megtervezett fájlstruktúra is megteszi. Néha a legegyszerűbb absztrakció a leghatékonyabb.


Ez a bejegyzés a Codumentor kódbázisának tényleges implementációján alapul. Minden kódrészlet éles kódból származik.