From answering questions to solving problems β Phase 6 of the Jarvis AI Platform
After Phase 5, Jarvis could hear, speak, remember conversations, retrieve documents, and use tools. But every interaction was still limited to a single request and a single response.
You: "What's the weather in Kathmandu?"
Whisper
β
AiOrchestrator
β
WeatherTool
β
Text-to-Speech
Jarvis:
"It is 22Β°C and clear."
That works well for simple questions.
It completely breaks down when a task requires multiple decisions.
Imagine asking:
Research the top 3 Java AI frameworks,
compare them,
and summarize the findings.
A traditional chatbot usually replies:
I don't have enough information to research that.
The problem isn't intelligence.
The problem is planning.
To answer properly, the AI must:
That requires multiple tool calls and reasoning between each one.
This is exactly what AI agents are designed to do.
ReACT stands for:
Reason + Act
Instead of generating one response, the AI repeatedly performs a reasoning loop.
THINK
β
ACT
β
OBSERVE
β
THINK
β
ACT
β
OBSERVE
β
FINAL ANSWER
Example:
THOUGHT:
I should search for Java AI frameworks.
ACTION:
search
INPUT:
Java AI frameworks 2026
β
OBSERVATION:
Spring AI
LangChain4j
Semantic Kernel
β
THOUGHT:
Now I need comparison data.
β
ACTION:
search
INPUT:
Spring AI vs LangChain4j
β
FINAL ANSWER
Instead of guessing everything up front, the AI gathers information step by step before producing the final response.
The most important design decision of Phase 6 was not modifying the existing chat pipeline.
Instead of turning AiOrchestrator
into a giant class responsible for both chat and agents, agents became a completely separate orchestration layer.
β Wrong
AiOrchestrator
β
Single Chat
β
Agent Logic
β
Tool Logic
β
Everything Mixed Together
β
Correct
AgentController
β
AgentOrchestrator
β
AgentExecutor
β
AgentPlanner
β
ToolRegistry
AiOrchestrator
β
Remains Completely Unchanged
Everything built during Phases 1β5 continues working exactly as before.
Agents simply reuse the existing tools.
The final architecture looks like this.
AgentController
β
AgentOrchestrator
β
AgentExecutor
β
AgentPlanner
β
ToolRegistry
Each component has a single responsibility.
Keeping these responsibilities isolated made the implementation significantly easier to maintain.
The planner doesn't simply ask the AI for an answer.
Instead, it asks for structured output.
You are an AI agent.
Available tools:
- getWeather
- calculate
- search
For every step respond exactly as:
THOUGHT:
...
ACTION:
...
INPUT:
...
When enough information has been gathered:
THOUGHT:
...
FINAL_ANSWER:
...
This prompt acts as a contract between the model and the parser.
The first implementation used indexOf()
.
response.indexOf("ACTION:");
That failed whenever the literal text ACTION:
appeared inside user data.
The solution was precompiled regular expressions anchored to the beginning of each line.
private static final Pattern ACTION_PATTERN =
Pattern.compile(
"(?ms)^ACTION:\\s*(.*?)"
+ "(?=^(?:THOUGHT:|INPUT:|FINAL_ANSWER:)|\\z)");
This guarantees that only real section headers are parsed.
The executor coordinates the complete lifecycle.
public Flux<AgentEvent> execute(
Agent agent,
UUID userId) {
return Flux.create(sink ->
runLoop(sink, agent, userId))
.subscribeOn(Schedulers.boundedElastic())
.timeout(TOTAL_TIMEOUT);
}
A few design decisions are worth highlighting.
Flux.generate()
allows only one event per iteration.
Agents frequently emit multiple events:
Flux.create()
supports that naturally.
Planning calls, database writes, and tool execution are blocking operations.
Moving the entire loop onto boundedElastic()
keeps the WebFlux event loop free.
Every agent is protected by:
Agents can never run forever.
Initially each event incremented the step counter independently.
THINK β Step 0
ACT β Step 1
OBSERVE β Step 2
Those three events actually belong to the same logical step.
The fix was simple.
Capture the current step once.
final int currentStep = stepIndex;
emitThink(currentStep);
emitAct(currentStep);
emitObserve(currentStep);
stepIndex++;
Now every event generated during one reasoning cycle shares the same step number.
Originally tool dispatch used substring matching.
method.contains(toolName)
This produced unexpected matches.
search
β
webSearch
β
searchDocuments
The correct implementation performs exact matching.
method.equalsIgnoreCase(toolName.trim())
Because the system prompt already specifies the exact method names, exact matching is both safer and simpler.
Agents execute for much longer than a normal chat response.
The browser shouldn't wait until everything finishes.
Instead, every reasoning step is streamed immediately.
event: think
event: act
event: observe
event: final
event: done
Users can literally watch the AI think.
One subtle problem appeared during testing.
If a browser tab closed, the agent continued executing in the background.
The fix required checking cancellation inside every loop iteration.
if (sink.isCancelled()) {
return;
}
One small check prevents wasted CPU time and unnecessary background work.
Agents move through a strict lifecycle.
PENDING
β
RUNNING
β
COMPLETED
or
FAILED
or
CANCELLED
Invalid transitions are rejected directly by the domain model.
agent.withRunning();
agent.withCompleted();
agent.withFailed();
The service layer doesn't enforce state rules.
The domain object does.
Concurrent updates introduced another challenge.
A completion event and an error event could arrive simultaneously.
Instead of overwriting each other, updates use compare-and-set semantics.
UPDATE agents
SET status = :newStatus
WHERE id = :id
AND status = :expectedStatus
If another thread already changed the state, zero rows are updated.
No race conditions.
No silent overwrites.
The complete agent system exposes six endpoints.
POST /api/v1/agents/stream
POST /api/v1/agents
GET /api/v1/agents
GET /api/v1/agents/{id}
GET /api/v1/agents/{id}/steps
DELETE /api/v1/agents/{id}
The streaming endpoint returns live ReACT events while the asynchronous endpoint starts long-running agents without holding the HTTP request open.
User
β
"What is the weather in London
and Tokyo,
and what time is it there?"
β
THINK
β
Weather Tool
β
Time Tool
β
Weather Tool
β
Time Tool
β
FINAL ANSWER
One request.
Multiple tools.
One coherent response.
No Python.
No LangChain.
Pure Java with Spring AI.
The parser expects a specific format.
Any ambiguity breaks the workflow.
Prompt engineering matters just as much as parser implementation.
AI models occasionally produce malformed output.
Rather than failing, Jarvis treats unknown responses as a final answer and continues.
The Agent
object owns its lifecycle.
Impossible transitions become impossible states.
Multiple asynchronous events may update the same row.
Checking the expected state inside SQL eliminates lost updates.
Running on an Intel Core Ultra 7 with 16 GB RAM:
| Operation | Typical Time |
|---|---|
| Agent creation | ~10 ms |
| AI planning | 2β8 s |
| Tool execution | 50β500 ms |
| Step persistence | ~10 ms |
| Typical 3-step task | 10β25 s |
The AI planning phase dominates overall execution time.
Phase 7 introduces the complete web interface.
It brings together everything built so far:
The backend is complete.
The next challenge is building the frontend.
Jarvis is open source under the Apache 2.0 License.
Current contributor-friendly issues include:
#84 CLI agent commands
#85 Agent REST API integration tests
#66 CLI tool commands
#34 CLI memory commands
GitHub:
https://github.com/sujankim/jarvis-ai-platform
Your AI. Your Data. Your Machine.