12 KiB
AgenticCode — Funktionsweise, Verwendung, Grenzen
Technische White-Box-Übersicht. AgenticCode ist ein Server, der Quellcode — Software AG Natural und Java — in einen einheitlichen Graphen zerlegt, in Neo4j ablegt, semantisch anreichert und über eine agenten-optimierte Schnittstelle (REST + CLI) abfragbar macht.
Grundlage: statische Analyse des Repositorys
acmit AgenticCode selbst (Dogfooding). Stand: Juli 2026 · Server-Version 8. Die Beschreibung folgt dem tatsächlichen Code, nicht einer Spezifikation.
Tech-Stack: Quarkus · Java 21 (Virtual Threads) · Neo4j 5 · Mutiny (reactive) · NullAway · JavaParser + eigener Natural-Parser.
1. Wie AgenticCode funktioniert (White Box)
Der Kern ist eine Pipeline: jede Quelldatei wird geparst, in ein sprachunabhängiges AST-Modell überführt, als Knoten-/Kanten-Graph in Neo4j persistiert, durch mehrere Anreicherungs-Schritte (Enrichment) verknüpft und über REST und CLI verfügbar gemacht.
Quelldateien → Parser (Natural/Java) → Unified AST → Neo4j-Graph → Enrichment → Agenten-API
Modul-Aufbau (Maven, mehrschichtig)
| Modul | Aufgabe |
|---|---|
ac-parser-core |
Gemeinsames AST-Schema (NodeType, EdgeType), SPIs (LanguageParser, CoarseScanner), SourceHash. |
ac-parser-natural |
Eigener Parser für Software AG Natural — es gibt keinen brauchbaren OSS-Parser. Zeilen-/Regex-basiert. Plus lexer-basierter Grob-Scanner (Tier 1). |
ac-parser-java |
Java über die JavaParser-Bibliothek (mit Symbol-Auflösung). Grob-Scan als Projektion des vollen Parses. |
ac-neo4j-store |
Persistenz & alle Cypher-Abfragen (GraphRepository, CypherQueries), Enrichment-Schritte. |
ac-code-server |
Quarkus-App: REST-Ressourcen, Dienste (Ingest, Deep-Ingest-Koordinator, Source-Snippets). |
ac-cli |
Kommandozeilen-Client (picocli) — dünner Wrapper über die REST-API. |
Das einheitliche AST-Modell
Jeder Knoten hat id, sourceFile, language, startLine, endLine. Knoten werden
über (type, name, sourceFile, project) zusammengeführt (MERGE). Unaufgelöste
Referenzen entstehen als Platzhalter mit leerem sourceFile.
- Knotentypen:
MODULE,FUNCTION,VARIABLE,DATA_STRUCTURE,DB_TABLE,FIELD,CONSTANT,DB_ACCESS,CONTROL_FLOW - Kantentypen:
CONTAINS,CALLS,READS,WRITES,USES_TYPE,EXTENDS,IMPLEMENTS,INCLUDES,ARG_TO_PARAM,INJECTS,REFERENCES,MAPS_TO
Das Drei-Stufen-Ingest-Modell (lazy)
Statt alles eager zu parsen, wird nur so tief analysiert, wie eine Abfrage es verlangt. Das hält das Anlegen billig und verteilt die teure Arbeit auf den tatsächlichen Bedarf.
- Tier 1 — Grob-Index (Lexer), beim Anlegen. Beim Projekt-Anlegen wird jede Datei
lexer-artig gescannt: Modul-/Funktions-Hüllen, Identifier-Index, grobe
CALLS/READS/WRITES/INCLUDES(inkl. dynamischerCALLNAT-Ziele, Copycode), plussourceHash. Module landen alsCALL_GRAPH/NOT_INGESTED. Billig. - Tier 2 — Tiefer Ingest, bei Bedarf. Feld-/Datenfluss-Abfragen lösen automatisch
einen tiefen Ingest des Moduls samt Abhängigkeitsbaum aus: Kontrollfluss, Datenfluss,
ARG_TO_PARAM, dynamischer Dispatch. Module werdenFULL/INGESTED. - Tier 3 — Quelltext von Platte, beim Lesen. Quelltext wird nicht im Graphen
gespeichert, sondern beim Lesen aus der Datei geschnitten. Ein Content-Hash
(
sourceHash) je Datei erkennt Änderungen; bei Abweichung kommtSTALE_SOURCEstatt falscher Zeilen.
Enrichment — wo die Semantik entsteht
Nach dem Persistieren läuft eine geordnete Kette von Cypher-Schritten (jeder idempotent, je eigene Transaktion):
- Platzhalter-Auflösung: Referenz-Kanten (
CALLS,INCLUDES,EXTENDS, …) werden auf reale Knoten umgehängt, sobald das Zielmodul existiert. - Dynamisches
CALLNAT <var>: per Konstantenpropagation werden Literal-Ziele einer Dispatch-Variablen aufgelöst — intra-Modul und (über Datenfluss) cross-Modul. - Feld-Auflösung & Datenfluss: Feld-Platzhalter aus
USING/Copycode werden aufgelöst;ARG_TO_PARAMverbindet Aufruf-Argumente mit Callee-Parametern (Basis fürflow-forward/backward). - Polymorphie (Java, CHA): Aufrufe auf Interface/Basistyp werden auf Implementierungen/Subklassen aufgefächert.
- Unresolved-Markierung: verbleibende Platzhalter werden mit
unresolved = true/falsemarkiert (je nachdem, ob eine echte Definition existiert).
Ingest-Status & Nebenläufigkeit
Jedes reale Modul trägt einen dauerhaften Lebenszyklus:
NOT_INGESTED → INGESTING → INGESTED. Lösen zwei Abfragen denselben Deep-Ingest aus,
verschmelzen sie statt doppelt zu arbeiten: innerhalb eines Prozesses per
In-Process-Lock, prozessübergreifend per Best-Effort-DB-Claim mit Stale-Reclaim. Ein
globaler Semaphor (max-concurrent-warms, Default 2) deckelt parallele Parses; ist er
gesättigt, liefert die Abfrage sofort die flachere Tier-1-Antwort statt zu blockieren.
Grenzen des tiefen Ingests: maxDepth (Default 5, max 20 Hops) und maxNodes
(Default 300 Dateien) — bei Überschreitung wird abgeschnitten und ein truncation-Hinweis
mitgeliefert.
2. Verwendung: CLI, API, Agenten
Es gibt zwei gleichwertige Zugänge auf dieselbe Logik. Für KI-Agenten ist die empfohlene Reihenfolge: REST zuerst, dann CLI, und erst als letztes klassisches Durchsuchen (grep).
- REST (programmatisch, auch für Agenten): JAX-RS unter
/api/projects/{p}/…. Kompaktes JSON, Pagination vialimit/offset. - CLI (manuell/Skripte):
ac <befehl>— dünner Wrapper über REST, für schnelle Checks am Terminal.
Was man abfragen kann
| Zweck | REST-Endpunkt | CLI |
|---|---|---|
| Wer ruft auf? | /modules/{n}/callers |
ac callers |
| Was wird aufgerufen? | /modules/{n}/callees |
ac callees |
| Transitiver Aufrufbaum | /modules/{n}/call-tree |
ac call-tree |
| Modul-Überblick | /modules/{n}/context |
ac context |
| DB-Zugriffe | /modules/{n}/db-accesses |
ac db-accesses |
| Bezeichner suchen | /search/identifier |
ac search-identifier |
| Datenfluss verfolgen | /variables/{n}/flow-forward · /field-flow |
ac flow-forward |
| Re-Ingest nach Änderung | POST /refresh |
ac refresh |
Typischer Ablauf
# 1 · Projekt anlegen — löst automatisch den Tier-1-Grob-Scan aus
curl -X POST http://localhost:8787/api/projects/mein-projekt \
-H 'Content-Type: application/json' \
-d '{"root":"/pfad/zum/code","excludeDirs":["target"]}'
# 2 · Abfragen — ein tiefer Ingest passiert bei Bedarf automatisch
curl http://localhost:8787/api/projects/mein-projekt/modules/KUNDE/callers
# 3 · Nach Code-Änderungen: refreshen (ganzes Projekt oder ein Modul)
curl -X POST http://localhost:8787/api/projects/mein-projekt/refresh # Call-Graph-Ebene
curl -X POST http://localhost:8787/api/projects/mein-projekt/refresh?deep=true # volle Feld-Ebene
Dasselbe per CLI
ac connect http://localhost:8787
ac use mein-projekt
ac callers KUNDE
ac call-tree KUNDE --depth 3
ac db-accesses KUNDE
ac refresh # bzw. ac refresh KUNDE für ein Modul
Kernidee für Agenten: Man muss nie einen Ingest von Hand anstoßen. Anlegen erzeugt den Grob-Index; jede Feld-/Fluss-Abfrage vertieft transparent genau die berührten Module. Nur nach echten Datei-Änderungen ist ein
refreshnötig.
3. Grenzen & Schwächen
Ehrliche Einordnung. Das meiste folgt bewusst aus dem Design (Heuristik statt Vollständigkeit, lazy statt eager); einiges sind offene Baustellen. Legende: [Design] = bewusste Näherung · [Limit] = echte Einschränkung · [Umfang] = Reichweite.
-
[Design] Natural-Parser ist heuristisch, keine vollständige Grammatik. Der Parser ist zeilen-/regex-basiert und erkennt priorisierte Konstrukte, nicht die ganze Sprache.
PROGRAMvs.SUBPROGRAMist eine Heuristik (Vorhandensein einerPARAMETER-Sektion), da flacher Quelltext die Katalog-Metadaten nicht trägt. Exotische Statements/Makros können übersehen werden. -
[Design] Dynamischer Dispatch wird approximiert, nicht bewiesen.
CALLNAT <var>wird über Konstantenpropagation aufgelöst — jeder Literalwert, den die Variable annehmen kann, wird zum Callee (Über-Approximation, korrekt für Dispatcher). Zur Laufzeit berechnete Ziele, die kein Literal sind, bleiben unaufgelöst: sie erscheinen als markierterunresolved-Platzhalter, nicht als aufgelöste Kante. -
[Limit] Keine automatische Invalidierung —
refreshist manuell. Ändert sich eine Datei, liefernsource-EndpunkteSTALE_SOURCEbis zum nächstenrefresh. Gelöschte Dateien hinterlassen verwaiste Knoten, weilrefreshmergt statt zu wischen (ein sauberer Neustand erfordert Löschen + Neu-Anlegen). File-Watch/Auto-Invalidierung ist bewusst zurückgestellt. -
[Limit] Vollständigkeit hängt an der Ingest-Tiefe. Auf Call-Graph-Ebene ist
callersnur so vollständig wie der Grob-Index: ein Aufrufer, der ausschließlich dynamisch dispatcht, wird erst nach tiefem Ingest sichtbar. Unter Last (Semaphor gesättigt) können Abfragen vorübergehend die flachere Tier-1-Antwort liefern. Und beitruncation(Tiefen-/Knoten-Limit) ist der Abhängigkeitsbaum unvollständig. -
[Design] Datenfluss ist Argument→Parameter, nicht voll wert-/pfad-sensitiv.
flow-forward/backwardfolgtARG_TO_PARAM- sowieREADS/WRITES-Kanten. Das ist eine strukturelle Näherung — keine vollständige Datenfluss-Analyse über beliebige Ausdrücke, Bedingungen oder Aliasing. Copycode/INCLUDE-Quelltext-Ausschnitte sind roher Text vor der Expansion. -
[Limit] Java ohne Classpath · gleiche Namen als Duplikate. Die Java-Symbolauflösung läuft ohne vollen Classpath — projektfremde Typen (JDK, Frameworks) bleiben bewusst unaufgelöst (Filter für externe Typen). Definiert derselbe Modul-/Klassenname in mehreren Dateien dieselbe Identität, wird das als Duplikat gemeldet und nicht ingestiert (im Repo
acz. B. 10 Fälle) — statt willkürlich eine Datei zu wählen. -
[Limit] Node-IDs sind nicht stabil ·
unresolvednur punktuell. Knoten-ids werden bei jedem Re-Ingest neu vergeben — gecachte IDs veralten. Dasunresolved-Flag wird explizit nur von/search/identifierund/nodes/{id}ausgegeben; incallers/calleesist es nur indirekt (leeressourceFile) erkennbar. -
[Umfang] Zwei Sprachen · offene Performance-Baustellen. Unterstützt werden ausschließlich Natural und Java. Ein tiefer Ganz-Projekt-Ingest ist auf großen Codebasen teuer (Enrichment im Minutenbereich) — genau deshalb das lazy Modell. Paralleles Parsen und gebündeltes Persistieren sind noch offen.