Files
agenticCode/x-docs/agenticcode-ueberblick.md
Ingo Schnabel 2c56eea161 Remove MCP
2026-08-04 12:57:08 +02:00

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 ac mit 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. dynamischer CALLNAT-Ziele, Copycode), plus sourceHash. Module landen als CALL_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 werden FULL / 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 kommt STALE_SOURCE statt 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_PARAM verbindet Aufruf-Argumente mit Callee-Parametern (Basis für flow-forward/backward).
  • Polymorphie (Java, CHA): Aufrufe auf Interface/Basistyp werden auf Implementierungen/Subklassen aufgefächert.
  • Unresolved-Markierung: verbleibende Platzhalter werden mit unresolved = true/false markiert (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 via limit/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 refresh nö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. PROGRAM vs. SUBPROGRAM ist eine Heuristik (Vorhandensein einer PARAMETER-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 markierter unresolved-Platzhalter, nicht als aufgelöste Kante.

  • [Limit] Keine automatische Invalidierung — refresh ist manuell. Ändert sich eine Datei, liefern source-Endpunkte STALE_SOURCE bis zum nächsten refresh. Gelöschte Dateien hinterlassen verwaiste Knoten, weil refresh mergt 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 callers nur 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 bei truncation (Tiefen-/Knoten-Limit) ist der Abhängigkeitsbaum unvollständig.

  • [Design] Datenfluss ist Argument→Parameter, nicht voll wert-/pfad-sensitiv. flow-forward/backward folgt ARG_TO_PARAM- sowie READS/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 ac z. B. 10 Fälle) — statt willkürlich eine Datei zu wählen.

  • [Limit] Node-IDs sind nicht stabil · unresolved nur punktuell. Knoten-ids werden bei jedem Re-Ingest neu vergeben — gecachte IDs veralten. Das unresolved-Flag wird explizit nur von /search/identifier und /nodes/{id} ausgegeben; in callers/callees ist es nur indirekt (leeres sourceFile) 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.