ReAct, Plan-and-Execute, Multi-Agent svi podržavaju paralelnost, efikasnost izvršenja Agent-a je maksimalno povećana
PaiCLI je već ažuriran na 7. epizodu, ReAct, Plan-and-Execute, Memory, RAG, Multi-Agent, HITL, sve što treba postoji.
Ali jedan problem nije rešen — serijski.
Neka PaiCLI pomogne da pročitamo tri fajla, on će pošteno pročitati prvi, zatim drugi, zatim treći. Tri fajla između sebe nemaju nikakvih zavisnosti, mogu se čitati istovremeno, ali Agent baš hoće da stane.
Plan-and-Execute mod je još očigledniji. Pet zadataka izdvoji, prva dva međusobno ne zavise, treći zavisi od rezultata prvih dva. Po logici prva dva treba da trče istovremeno, ali sada se prvo završi pa tek onda drugi.
Multi-Agent je isto, dva Worker-a su miruju, ali orkestrator dodeljuje samo jednom, drugi čeka.
Danas, PaiCLI ćemo modifikovati iz serijskog u paralelni. Nakon modifikacije, tri izvršna puta — ReAct, Plan-and-Execute, Multi-Agent — svi podržavaju paralelno izvršenje, efikasnost je maksimalna.
01, Gde se može paralelizovati?
U PaiCLI postoji tri scenarija gde se može paralelizovati.
Prvi je paralelnost poziva alata. LLM u jednom odgovoru vrati više tool_calls, ovi alati međusobno nemaju zavisnosti, mogu se izvršiti istovremeno.
Na primer LLM kaže „Hoću istovremeno da pročitam pom.xml i README.md“, vraća dva read_file poziva, nema potrebe da čekaš da se prvi pročita pa tek onda drugi.

Drugi je paralelnost zadataka u Plan-and-Execute modu. Zadaci u istom batch-u u DAG-u nemaju međusobne zavisnosti, mogu se istovremeno baciti LLM-u na izvršenje.
Treci je paralelnost Worker-a u Multi-Agent modu. Orkestrator otkrije grupu nezavisnih koraka, može istovremeno dodeliti više Worker-a da trče.
02, Paralelno izvršenje alata
Prvo pogledaj ToolRegistry pre modifikacije:
public String executeTool(String name, String argumentsJson) {
Tool tool = tools.get(name);
if (tool == null) {
return "Nepoznat alat: " + name;
}
// Parsiranje parametara, izvršenje alata
JsonNode args = mapper.readTree(argumentsJson);
Map<String, String> argMap = new HashMap<>();
args.fields().forEachRemaining(entry ->
argMap.put(entry.getKey(), entry.getValue().asText()));
return tool.executor().execute(argMap);
}Jedan poziv izvršava jedan alat, vraća jedan string. Agent dobije više tool_calls, poziva ovu metodu u petlji, serijski.
Sada treba dodati batch metodu executeTools, prima grupu poziva alata, paralelno izvrši pa vrati rezultate.
Prvo definiraj strukturu podataka.
ToolInvocation predstavlja jedan zahtev za poziv alata, ToolExecutionResult predstavlja rezultat izvršenja:
public record ToolInvocation(String id, String name, String argumentsJson) {}
public record ToolExecutionResult(String id, String name, String argumentsJson,
String result, long elapsedMillis, boolean timedOut) {
static ToolExecutionResult completed(ToolInvocation inv, String result, long elapsed) {
return new ToolExecutionResult(inv.id(), inv.name(), inv.argumentsJson(),
result, elapsed, false);
}
static ToolExecutionResult timedOut(ToolInvocation inv, long timeoutSeconds) {
return new ToolExecutionResult(inv.id(), inv.name(), inv.argumentsJson(),
"Izvršenje alata je timeout (" + timeoutSeconds + "s), otkazano",
timeoutSeconds * 1000, true);
}
static ToolExecutionResult failed(ToolInvocation inv, String message) {
return completed(inv, "Neuspelo izvršenje alata: " + message, 0);
}
}timedOut polje označava da li je timeout, pogodno za LLM da odluči da li će ponoviti pokušaj na osnovu timeout rezultata.
Ključna metoda executeTools:
private static final int MAX_PARALLEL_TOOLS = 4;
private static final int DEFAULT_TOOL_BATCH_TIMEOUT_SECONDS = 90;
public List<ToolExecutionResult> executeTools(List<ToolInvocation> invocations) {
if (invocations == null || invocations.isEmpty()) {
return List.of();
}
// Samo jedan alat, ne otvaram thread pool, direktno izvršim
if (invocations.size() == 1) {
ToolInvocation invocation = invocations.get(0);
long startedAt = System.nanoTime();
String result = executeTool(invocation.name(), invocation.argumentsJson());
return List.of(ToolExecutionResult.completed(invocation, result,
elapsedMillis(startedAt)));
}
int parallelism = Math.min(invocations.size(), MAX_PARALLEL_TOOLS);
ExecutorService executor = Executors.newFixedThreadPool(parallelism, r -> {
Thread thread = new Thread(r, "paicli-tool-executor");
thread.setDaemon(true);
return thread;
});
try {
List<Callable<ToolExecutionResult>> tasks = invocations.stream()
.<Callable<ToolExecutionResult>>map(invocation -> () -> {
long startedAt = System.nanoTime();
String result = executeTool(invocation.name(),
invocation.argumentsJson());
return ToolExecutionResult.completed(invocation, result,
elapsedMillis(startedAt));
})
.toList();
List<Future<ToolExecutionResult>> futures =
executor.invokeAll(tasks, toolBatchTimeoutSeconds, TimeUnit.SECONDS);
List<ToolExecutionResult> results = new ArrayList<>();
for (int i = 0; i < futures.size(); i++) {
Future<ToolExecutionResult> future = futures.get(i);
ToolInvocation invocation = invocations.get(i);
if (future.isCancelled()) {
results.add(ToolExecutionResult.timedOut(invocation,
toolBatchTimeoutSeconds));
continue;
}
results.add(future.get());
}
return results;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return invocations.stream()
.map(inv -> ToolExecutionResult.failed(inv, "Batch izvršenje alata je prekinuto"))
.toList();
} finally {
executor.shutdownNow();
}
}Jedan alat ne otvara thread pool
Kada postoji samo jedan alat, izvrši se direktno u trenutnoj niti, preskoči thread pool.
Kreiranje thread pool-a samo po sebi ima troškove — kreiranje niti, planiranje zadataka, Future pakovanje, za jedan alat je to gubitak resursa.
U ReAct modu LLM većinom vraća samo jedan tool_call.
Ograničenje maksimalne paralelnosti
MAX_PARALLEL_TOOLS = 4, najviše istovremeno 4 alata.
Zašto ne pustiti da se otvori onoliko niti koliko ima poziva alata?
Zato što izvršenje alata je na dnu IO operacija — čitanje fajla, pokretanje komande, mrežni zahtev. Istovremeno previše niti, na nivou operativnog sistema IO konkurencija će suprotno usporiti ukupnu brzinu.

invokeAll uniformni timeout
Koristi executor.invokeAll(tasks, timeout, TimeUnit.SECONDS) umesto da svaki Future zasebno postavi timeout.
Dobra strana invokeAll je što ako ceo batch prekorači timeout, nezavršeni zadaci će automatski biti otkazani (future.isCancelled() vraća true).
Već završeni alati nisu pogođeni, rezultati se normalno uzimaju.
Batch timeout je postavljen na 90 sekundi, single komanda timeout je 60 sekundi.
Ova dva timeout-a su nezavisna — execute_command alat interno ima svoj limit od 60 sekundi, batch timeout je sigurnosna mera, sprečava da jedan alat potpuno zakuji ceo Agent.
Očuvanje redosleda rezultata
Redosled povratnih rezultata mora biti konzistentan sa redosledom ulaznih invocations. Ovo je veoma važno.
LLM vraća tool_calls u određenom redosledu, Agent mora rezultate alata da vrati u istoriju poruka u istom redosledu. Ako se redosled poremeti, LLM će u sledećem krugu zaključivanja biti zabunjen koji rezultat odgovara kojem pozivu alata.
invokeAll garantuje da je lista futures potpuno konzistentna sa listom tasks po redosledu.
03, Kako Agent.java uključuje paralelnost
ReAct petlja u Agent.java, vrlo mala promena.
Pre se pozivalo executeTool u petlji:
for (GLMClient.ToolCall toolCall : response.toolCalls()) {
String result = toolRegistry.executeTool(
toolCall.function().name(),
toolCall.function().arguments());
conversationHistory.add(GLMClient.Message.tool(toolCall.id(), result));
}Sada se menja u batch poziv:
private List<ToolExecutionResult> executeToolCalls(
List<GLMClient.ToolCall> toolCalls, int iteration) {
List<ToolInvocation> invocations = new ArrayList<>();
for (GLMClient.ToolCall toolCall : toolCalls) {
String toolName = toolCall.function().name();
String toolArgs = toolCall.function().arguments();
log.info("Scheduling tool: {} (iteration={})", toolName, iteration);
invocations.add(new ToolInvocation(toolCall.id(), toolName, toolArgs));
}
if (invocations.size() > 1) {
log.info("Executing {} tool calls in parallel (iteration={})",
invocations.size(), iteration);
}
return toolRegistry.executeTools(invocations);
}Pozivaoc samo treba da pretvori listu toolCalls u listu ToolInvocation, baca executeTools, dobija rezultate i vraća po redosledu.
List<ToolExecutionResult> toolResults = executeToolCalls(
response.toolCalls(), iteration);
for (ToolExecutionResult toolResult : toolResults) {
memoryManager.addToolResult(toolResult.name(), toolResult.result());
conversationHistory.add(GLMClient.Message.tool(
toolResult.id(), toolResult.result()));
}Vrlo malo izmena, jer je cela paralelna kompleksnost sakrivena u ToolRegistry.executeTools.
Agent ne mora da da li su alati izvršeni serijski ili paralelno, njemu je bitno samo „dobijem grupu poziva, vratim grupu rezultata“.

Još jedan detalj: u sistemski prompt je dodata jedna rečenica.
Kada LLM vrati više poziva alata u istom krugu, sistem će ih izvršiti paralelno;
ako alati imaju međuzavisnosti, molim te da ih pozivaš u više krugova.Reci LLM-u „možeš vratiti više poziva alata odjednom, mi ćemo ih izvršiti paralelno“. U isto vreme podseti ga „alati sa zavisnostima ne stavljaj u isti krug“. LLM-ova sposobnost sledenja instrukcija je u ovoj stvari veoma pouzdana.
04, DAG paralelnost u Plan-and-Execute
Paralelnost ReAct je na nivou alata, granularnost je mala. Paralelnost Plan-and-Execute je na nivou zadataka, granularnost je veća.
Vratimo se strukturi ExecutionPlan. Svaki Task ima listu zavisnosti, ceo plan formira DAG. Zadaci u DAG-u bez međusobnih zavisnosti mogu se izvršiti istovremeno.
Metoda getExecutionBatches() deli DAG po slojevima zavisnosti u batch-eve:
public List<List<Task>> getExecutionBatches() {
Map<String, Task> remaining = new LinkedHashMap<>(tasks);
Set<String> completed = new HashSet<>();
List<List<Task>> batches = new ArrayList<>();
while (!remaining.isEmpty()) {
List<Task> batch = remaining.values().stream()
.filter(task -> completed.containsAll(task.getDependencies()))
.toList();
if (batch.isEmpty()) {
break; // postoji ciklus ili nezadovoljene zavisnosti, izlaz
}
batches.add(batch);
for (Task task : batch) {
remaining.remove(task.getId());
completed.add(task.getId());
}
}
return batches;
}Na primer, pet zadataka međuzavisnosti je ovakva:
task_1 (bez zavisnosti)
task_2 (bez zavisnosti)
task_3 (zavisi od task_1)
task_4 (zavisi od task_1, task_2)
task_5 (zavisi od task_3, task_4)Podeljeno u tri batch-a: [task_1, task_2] → [task_3, task_4] → [task_5]. Prvi batch dva zadatka trče istovremeno, kad se završe istovremeno trče drugi batch, na kraju treći.

Metoda executeTaskBatch u PlanExecuteAgent obrađuje paralelno izvršenje:
private List<TaskExecutionResult> executeTaskBatch(
ExecutionPlan plan, List<Task> executableTasks,
StreamState streamState) {
// Single-task batch: direktno serijsko izvršenje, bez thread pool-a
if (executableTasks.size() == 1) {
Task task = executableTasks.get(0);
System.out.println("▶️ Izvršavam zadatak [" + task.getId() + "]: "
+ task.getDescription());
task.markStarted();
return List.of(TaskExecutionResult.success(task,
executeTask(plan.getGoal(), plan, task, streamState, System.out)));
}
// Multi-task batch: paralelno izvršenje
System.out.println("⚡ Ovaj batch paralelno izvršavam "
+ executableTasks.size()
+ " zadataka: " + parallelTaskIds);
ExecutorService executor = Executors.newFixedThreadPool(
Math.min(executableTasks.size(), 4), r -> {
Thread t = new Thread(r, "paicli-plan-executor");
t.setDaemon(true);
return t;
});
Map<String, ByteArrayOutputStream> buffers = new LinkedHashMap<>();
List<Future<TaskExecutionResult>> futures = new ArrayList<>();
for (Task task : executableTasks) {
task.markStarted();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
buffers.put(task.getId(), baos);
PrintStream taskOut = new PrintStream(baos, true, StandardCharsets.UTF_8);
futures.add(executor.submit(() -> {
return TaskExecutionResult.success(task,
executeTask(plan.getGoal(), plan, task, streamState, taskOut));
}));
}
// Čekaj da se svi zadaci završe, flush-uj bafer po redosledu
for (Task task : executableTasks) {
ByteArrayOutputStream buf = buffers.get(task.getId());
if (buf != null && buf.size() > 0) {
System.out.print(buf.toString(StandardCharsets.UTF_8));
System.out.flush();
}
}
return results;
}Ovde postoji veoma važan dizajn — baferiranje striming izlaza.
Izlaz svakog paralelnog zadatka ne piše se direktno u System.out, nego u odgovarajući ByteArrayOutputStream bafer.
Nakon što se svi zadaci završe, tek onda flush-uj bafer u standardni izlaz po redosledu ID-ova zadataka.
Zašto ovako?
Zato što ako više niti istovremeno pišu u System.out, izlaz će se promešati. Korisnik u terminalu će videti promešanu gomilu, proces mišljenja task_1 isprepliću sa rezultatima alata task_2, potpuno nečitljivo.

Nakon baferiranja, redosled izlaza koji korisnik vidi je fiksan — prvo kompletnan proces izvršenja task_1, zatim task_2, jasno i razumljivo.
Kada je batch samo jedan zadatak, ide direktno na System.out, održava osećaj pravog kucanja.
Samo kada je prava paralelnost više zadataka, onda se uključuje baferiranje.
05, Worker paralelnost u Multi-Agent-u
Paralelnost Multi-Agent-a je konzistentna sa Plan-and-Execute, ali dodaje jednu dodatnu kompleksnost — pool Worker-a.
U AgentOrchestrator postoje dva Worker-a: worker-1 i worker-2.
Isti Worker ne može biti istovremeno zauzet od dva koraka, jer Worker interno ima istoriju razgovora, konkurentno pisanje će dovesti do trke podataka.
Koristi BlockingQueue da napravi Worker pool:
BlockingQueue<SubAgent> workerPool = new LinkedBlockingQueue<>(workers);
for (ExecutionStep step : batch) {
futures.add(executor.submit(() -> {
SubAgent worker = null;
SubAgent localReviewer = new SubAgent(
"reviewer-" + step.id(), AgentRole.REVIEWER,
llmClient, toolRegistry);
try {
worker = workerPool.take(); // Pozajmi Worker iz pool-a
runStep(step, steps, retryCount, worker, localReviewer,
context, stepOut);
} finally {
if (worker != null) {
worker.clearHistory();
workerPool.offer(worker); // Vrati Worker u pool
}
}
}));
}workerPool.take() će blokirati, dok u pool-u ne postoji slobodan Worker. Ako tri koraka treba istovremeno da se izvrše, a postoje samo dva Workera, treći korak će čekati da se neki od prethodnih Workera završi.

Reviewer se obrađuje drugačije. Svaki paralelni korak kreira nezavisni Reviewer instancu, ne ide kroz pool.
Zašto Worker ide kroz pool, a Reviewer ne?
Broj Workera je namerno ograničen — dva Workera su dovoljna, previše Workera istovremeno poziva LLM će probiti API. Reviewer-ova istorija razgovora je kratka (samo jedan krug revizije), trošak kreiranja nove instance je minimalan, a Revieweri paralelnih koraka ako dele instancu, istorija razgovora će se konkurentno promešati.
Metod runStep se deli između serijskog i paralelnog puta, kroz PrintStream out parametar kontroliše destinaciju izlaza:
private void runStep(ExecutionStep step, List<ExecutionStep> steps,
Map<String, Integer> retryCount,
SubAgent worker, SubAgent reviewer,
String context, PrintStream out) {
out.println("🛠️ " + worker.getName() + " izvršava korak ["
+ step.id() + "]: " + step.description());
// Worker izvrši → Reviewer pregleda → najviše 2 puta ponovi
}Serijski put prosleđuje System.out, paralelni put prosleđuje PrintStream umotan u ByteArrayOutputStream.
06, Mehame timeout i otkazivanje
Paralelno izvršenje donosi jedan novi problem: ako se neki alat ili zadatak zakuje kako da bude?
Prethodno kod serijskog, execute_command ima 60 sekundi timeout, prekorači se prisilno ubija proces. Ali u paralelnom scenariju, timeout jednog alata ne sme uticati na druge alate.
executeTools koristi invokeAll timeout parametar da reši ovaj problem. Kad batch timeout prekorači, nezavršeni Future će biti otkazan:
List<Future<ToolExecutionResult>> futures =
executor.invokeAll(tasks, toolBatchTimeoutSeconds, TimeUnit.SECONDS);
for (int i = 0; i < futures.size(); i++) {
Future<ToolExecutionResult> future = futures.get(i);
if (future.isCancelled()) {
results.add(ToolExecutionResult.timedOut(invocation,
toolBatchTimeoutSeconds));
continue;
}
results.add(future.get());
}Alat koji je timeout vraća timedOut rezultat, LLM u sledećem krugu zaključivanja može videti „ovaj alat je timeout“, zatim odlučuje da li ponoviti pokušaj ili promeniti plan.

Tretman timeout unutar execute_command alata takođe zaslužuje da se spomene:
private String executeCommand(String command) {
Process process = pb.start();
boolean finished = process.waitFor(commandTimeoutSeconds, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
process.waitFor(2, TimeUnit.SECONDS);
return "Komanda je timeout (" + commandTimeoutSeconds + "s), prisilno obustavljena";
}
// ...
}Dva nivoa timeout-a su nezavisna.
Komandni nivo 60 sekundi timeout garantuje da jedna komanda neće beskonačno da trči, batch nivo 90 sekundi timeout garantuje da ceo paralelni batch ima sigurnosnu mrežu. U normalnim okolnostima komandni nivo timeout će se prvo okinuti, batch nivo je fallback.
07, Objedinjeni ulaz tri puta
Nakon modifikacije, ReAct, Plan-and-Execute, Multi-Agents tri izvršna puta svi koriste istu ToolRegistry.executeTools metodu.
| Izvršni put | Nivo paralelnosti | Način pozivanja |
|---|---|---|
| ReAct (Agent.java) | Nivo alata | LLM u jednom krugu vrati više tool_calls, paralelno izvršenje |
| Plan-and-Execute | Nivo zadataka | Paralelno izvršenje zadataka u istom batch-u DAG-a, alati unutar svakog zadataka takođe paralelni |
| Multi-Agent | Nivo Workera | Paralelna dodela koraka u istom batch-u zavisnosti Worker pool-u |
Plan-and-Execute i Multi-Agent su dvoslojna paralelnost: spoljašnji sloj je paralelnost na nivou zadataka/korakova (executeTaskBatch / runBatchParallel), unutrašnji sloj je paralelnost na nivou alata (executeTools).
Jedan paralelni zadatak u procesu izvršenja, LLM može vratiti više poziva alata, ovi pozivi će takođe biti obrađeni paralelno od strane executeTools. Dvostruka ugniježdenost, ali se međusobno ne ometaju, jer svaki sloj ima sopstveni thread pool.

08, Daemon niti i čišćenje resursa
Paralelno izvršenje uvodi thread pool, životni ciklus thread pool-a mora biti čisto obrađen.
Sve niti u svim thread pool-ovima su postavljene kao daemon niti:
Executors.newFixedThreadPool(parallelism, r -> {
Thread thread = new Thread(r, "paicli-tool-executor");
thread.setDaemon(true);
return thread;
});Daemon niti se automatski terminišu nakon što se sve non-daemon niti završe. To znači da kad korisnik pritisne Ctrl+C da izađe iz PaiCLI, niti za izvršenje alata neće sprečiti gašenje JVM-e.
Na kraju svake metode postoji executor.shutdownNow(), poziva se u finally bloku:
try {
// logika paralelnog izvršenja
} finally {
executor.shutdownNow();
}shutdownNow će prekinuti sve taskove koji se izvršavaju, zatim uništiti thread pool. Ne može se koristiti shutdown() — to je samo „prestati da primaš nove taskove“, već submit-ovani taskovi će i dalje da trče. Mi treba „odmah sve zaustavi“.
daemon niti + shutdownNow, dvostruko osiguranje, garantuje da thread pool neće curiti.
09, Ograničenje izlaza komande
Paralelno izvršenje još uvede jedan problem koji ranije nije bio očigledan: predugi izlaz komande će eksplodirati memoriju.
Serijski istovremeno trči samo jedna komanda, izlaz jedne komande ma koliko dug bio konačno je ograničen. Ali paralelno četiri komande istovremeno izlaze, ako svaka komanda izbaci desetak MB logova, memorija odmah eksplodira.
Zato metod readProcessOutput je dodao limit od 8000 karaktera:
private static final int MAX_COMMAND_OUTPUT_CHARS = 8_000;
private String readProcessOutput(Process process) throws Exception {
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (output.length() < MAX_COMMAND_OUTPUT_CHARS) {
output.append(line).append("\n");
}
}
}
if (output.length() >= MAX_COMMAND_OUTPUT_CHARS) {
return output.substring(0, MAX_COMMAND_OUTPUT_CHARS) + "\n...(izlaz je isečen)";
}
return output.toString();
}Deo koji prelazi 8000 karaktera će biti isečen, na kraju se dodaje obaveštenje „izlaz je isečen“. LLM vidi ovo obaveštenje i zna da izlaz nije potpun, može da odluči da li mu treba preciznija komanda da dobije potrebne informacije.

8000 karaktera je rezultat kompromisa. Prekratko, LLM neće dobiti dovoljno informacija; predugo, token potrošnja će eksplodirati (kontekst LLM-a se naplaćuje). Većini komandi efikasne informacije su u prvih nekoliko hiljada karaktera dovoljno.
10, Zabrani sveobuhvatno skeniranje
Još jedna bezbednosna promena. Paralelno izvršenje je ubrzalo Agent, ali veća brzina znači da ako se napravi glupost, posledice su ozbiljnije.
execute_command dodao je checklistu zabrana:
private boolean isDisallowedBroadScan(String command) {
String normalized = command.replaceAll("\\s+", " ")
.trim().toLowerCase(Locale.ROOT);
return normalized.contains("find /")
|| normalized.contains("find ~")
|| normalized.contains("find $home");
}Zabranjuje find /, find ~, find $HOME ove komande koje skeniraju ceo fajl sistem.
LLM ponekad radi „potpuno razume projekat“ će pokušati da skenira ceo disk, serijski je veoma nervirajuće (jedan find / može da trči par minuta), paralelno još manje podnošljivo. Četiri niti istovremeno find /, IO odmah je pun, sistem se usporava do pas.
Zabranjene komande će vratiti prijateljsku grešku, vođeno LLM da koristi read_file, list_dir, search_code ove alternative.
11, Probaj jednom da vidiš efekt
Pokreni PaiCLI, izvrši zadatak sa više korakova u /plan modu:
> /plan Analiziraj PaiCLI projekat: pročitaj pom.xml da razumeš zavisnosti, pročitaj README.md da razumeš funkcionalnosti, pročitaj ROADMAP.md da razumeš planiranje
Planer je podelio u tri zadatka, svi nemaju međuzavisnosti.
Prethodna verzija bi izvršavala serijski: pročitaj pom.xml → pročitaj README.md → pročitaj ROADMAP.md, ukupno vreme otprilike zbir tri LLM poziva.

Sada tri zadatka istovremeno počinju:
⚡ Ovaj batch paralelno izvršavam 3 zadatka: task_1, task_2, task_3
▶️ Paralelni zadatak [task_1]: Pročitaj pom.xml i analiziraj projekat zavisnosti
▶️ Paralelni zadatak [task_2]: Pročitaj README.md i razumemi funkcionalnosti
▶️ Paralelni zadatak [task_3]: Pročitaj ROADMAP.md i razumemi planiranjeVreme izvršenja tri zadatka se preklapa, ukupno vreme otprilike jednako vremenu najsporijeg zadatka.

U ReAct modu takođe može da se vidi efekat. Reci Agent-u da vidi strukturu projekta:
> Ista vam izlistaj fajlove u tri direktorijuma: src/main/java, src/test/java, src/main/resources
LLM će vratiti više list_dir poziva, ovi pozivi će biti paralelno izvršeni.
Multi-Agent mod je još očigledniji. U /team modu, orkestrator otkrije dva nezavisna koraka, istovremeno dodeli worker-1 i worker-2:
/team Molim te da podeliš 3 međusobno nezavisne zadatke u nezavisne korake i paralelno ih izvršiš:
1. Pročitaj pom.xml, objasni projekat zavisnosti i build konfiguraciju
2. Pročitaj README.md, objasni trenutno implementirane funkcionalnosti
3. Pročitaj ROADMAP.md, objasni dalje planiranje
Zahtev: Ova 3 zadatka čitanja ne smeju međusobno zavisiti, na kraju se sakupi.Dva Workera istovremeno rade, efikasnost se udvostručuje.

Kako napisati PaiCLI u životopis?
Naziv projekta: PaiCLI — Java Agent CLI
Opis projekta: Na osnovu ReAct paradigme od nule implementiran Agent alat komandne linije u Javi, integrisan Plan-and-Execute, Memory, RAG, Multi-Agent, HITL ljudsko odobrenje i asinhrono paralelno izvršenje, kompletno pokriva AI Agent tehnološki stack.
Ključne odgovornosti:
- Dizajnirao i implementirao objedinjeni paralelni motor izvršenja alata, koristeći
ExecutorService+ batch timeout, maksimalno 4 konkurentne staze, timeout alati se automatski otkazuju i vraćaju rezultati za LLM da ponovo donese odluku - Tri izvršna puta ReAct, Plan-and-Execute, Multi-Agent objedinjeno povezao sa paralelnim motorom
- U Plan-and-Execute modu implementirao DAG batch planiranje, nezavisne zadatke u istom sloju zavisnosti paralelno izvršava, kroz
ByteArrayOutputStreambaferiranje realizovano uređeno prikazivanje paralelnog izlaza - Koristeći
BlockingQueuerealizovao Multi-Agent Worker pool dodelu, garantuje da se isti Worker ne koristi konkurentno, Reviewer po koraku nezavisno kreira da se izbegne takmičenje istorije razgovora
