Pulse HK · engineering deep-dive

How we built a memory-first
multi‑agent shopping companion

Supervisor orchestration, A2A protocol, Mem0 long-term memory, and full observability — explained across eight pages.

Spring Boot 3 LangChain4j Mem0 A2A 0.3
System at a glance

Pulse HK exposes a single A2A endpoint. Inside, a supervisor orchestrates BrandFinder, OfferFinder, GeneralQA, while Mem0 stores cross‑session memories. Every request is monitored via AgentMonitor.

← Use left sidebar to explore Architecture, A2A, Memory, Agents, Identity, Monitor, Insights

Topology & wiring

The Spring Boot application intercepts A2A calls, resolves identity, injects memory context, and delegates to the supervisor. Sub‑agents are pure Java interfaces — LangChain4j generates proxies.

Pulse HK core flow
1
A2A message
2
UserContext resolve
3
Mem0 enrich
4
Supervisor + sub-agents
JavaAgentConfig.java
@Bean public PulseSupervisorAgent supervisorAgent(...) {
  return AgenticServices.supervisorBuilder(...)
    .chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(20))
    .listener(agentMonitor).build();
}

Agent‑to‑Agent standard

A2A enables discoverability via AgentCard, task lifecycle, and identity in metadata. Pulse HK’s endpoint integrates with any A2A client.

A2A UI — message/send
A2A UI
Web client invoking Pulse HK via JSON‑RPC
Task ID = short‑term planning window, userId = Mem0 scope → clean separation

Mem0 semantic loop

Memories are stored with topic metadata (brands, preferences, general). At retrieval, three parallel searches enrich the prompt. The chat history never sees system‑level memory messages → prevents hallucination.

User Query
➜
Mem0 Search
➜
Enrich Prompt
➜
Agent Execution
➜
Save Memory
JavaPulseChatMemoryStore.java
@Override public List<ChatMessage> getMessages(...) {
  return List.of(); // memories never injected as system messages
}

Plan‑then‑delegate flow

BrandFinder
extract brand, save memory
OfferFinder
tool call + memory write
Supervisor
final synthesis (20‑slot window)
AgenticScope writes userId once; sub‑agents read it without method parameters → clean interfaces.
JavaBrandFinderAgent.java
@Agent(name = "BrandFinderAgent", description = "extracts brand name")
String findBrandName(@V("userRequest") String request);

Three‑priority resolution

1. JWT Bearer2. X‑User‑Id header3. A2A metadata
JavaCustomAgentExecutor.java
UserContext ctx = UserContextResolver.resolve(message);
populateScope(ctx.userId(), ctx);
UserContextHolder.set(ctx);
// finally: double clear() in interceptor + executor
ThreadLocal + AgenticScope dual propagation guarantees identity across asynchronous agent loops.

AgentMonitor: full visibility

GET /report
HTML waterfall
GET /executions
nested tree
GET /summary
user stats + ongoing
A2A Inspector
inspector view
Live agent execution timeline
Registered with inheritedBySubagents=true → automatically tracks every sub‑agent call.

Six architectural invariants

🎯 Task ID as @MemoryId
Per‑request isolation
💬 Memories → prompt (not history)
Prevents hallucinated agent routing
🧹 Window sizes by contract
Deterministic token budgets
🔗 Dual identity propagation
ThreadLocal + AgenticScope
🌊 Topic‑segmented metadata
Brand/preferences/general buckets
📊 Zero‑surprise monitor
Inherited listener + HTML report
bashquick start
mem0 serve --port 8000
ollama run qwen3
./mvnw spring-boot:run
curl -X POST /offer/ -H "X-User-Id: alice" -d '...'