Skip to content

Commit 8219bba

Browse files
committed
docs: agent-first tutorial rewrite, human-in-the-loop, coordinator orchestration
Rewrite introduction as agent-first (not 3.x-centric); add @RequiresApproval tutorial with wire protocol and React example; expand coordinator with journal, handoffs, routing, eval, long-term memory, testing, LlmJudge; add @command confirm and console UI to agent docs
1 parent 62096b0 commit 8219bba

5 files changed

Lines changed: 446 additions & 71 deletions

File tree

docs/src/content/docs/agents/agent.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,32 @@ public String deploy(
109109

110110
Commands appear in the AI Console UI autocomplete and are also exposed via MCP as tools.
111111

112+
### Destructive Command Confirmation
113+
114+
For commands that perform destructive actions, use the `confirm` attribute to require user confirmation before execution:
115+
116+
```java
117+
@Command(value = "/reset", description = "Reset all data",
118+
confirm = "This will delete all data. Are you sure?")
119+
public String reset() {
120+
return dataService.resetAll();
121+
}
122+
```
123+
124+
The client receives a confirmation prompt before the command executes.
125+
126+
## Built-in Console UI
127+
128+
Every full-stack `@Agent` is automatically served by the **Atmosphere AI Console** at `/atmosphere/console/`. The console provides:
129+
130+
- Chat interface with streaming responses
131+
- Tool call visualization in the **AGENT COLLABORATION** panel
132+
- Approval prompts for `@RequiresApproval` tools
133+
- Command autocomplete
134+
- Connection status indicator
135+
136+
No frontend code needed — the console is bundled in `atmosphere-spring-boot-starter`.
137+
112138
## Tools
113139

114140
`@AiTool` methods are callable by the LLM during inference. They work identically across all backends (built-in, Spring AI, LangChain4j, Google ADK, Embabel).

docs/src/content/docs/agents/coordinator.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,120 @@ The same `@Coordinator` code works with any AI runtime. Switch the execution eng
343343
<artifactId>atmosphere-adk</artifactId>
344344
```
345345

346+
## Coordination Journal
347+
348+
Every coordination is automatically journaled — which agents were called, what they returned, timing, success/failure. The journal is a pluggable SPI (`CoordinationJournal`) with an in-memory default, discovered via `ServiceLoader`.
349+
350+
```java
351+
// After parallel execution, query the journal
352+
var events = fleet.journal().retrieve(coordinationId);
353+
var failed = fleet.journal().query(CoordinationQuery.forAgent("weather"));
354+
```
355+
356+
Event types: `Started`, `Dispatched`, `Completed`, `Failed`, `Evaluated`. Each event includes the agent name, skill/method called, timestamp, duration, and result summary.
357+
358+
## Agent Handoffs
359+
360+
An agent can transfer the conversation — with full history — to another agent. One method call:
361+
362+
```java
363+
@Prompt
364+
public void onPrompt(String message, StreamingSession session) {
365+
if (message.toLowerCase().contains("billing")) {
366+
session.handoff("billing", message); // conversation history travels with it
367+
return;
368+
}
369+
session.stream(message);
370+
}
371+
```
372+
373+
The client receives an `AiEvent.Handoff` event before the target agent responds. Nested handoffs are blocked (cycle guard). The target agent's `@Prompt` method runs with its own tools, system prompt, and interceptor chain.
374+
375+
## Conditional Routing
376+
377+
Route based on agent results — first match wins, with an optional fallback:
378+
379+
```java
380+
var weather = fleet.agent("weather").call("forecast", Map.of("city", city));
381+
382+
var result = fleet.route(weather, route -> route
383+
.when(r -> r.success() && r.text().contains("sunny"),
384+
f -> f.agent("outdoor").call("plan", Map.of()))
385+
.when(r -> r.success(),
386+
f -> f.agent("indoor").call("suggest", Map.of()))
387+
.otherwise(f -> AgentResult.failure("router", "route",
388+
"Weather unavailable", Duration.ZERO))
389+
);
390+
```
391+
392+
Every routing decision is recorded in the `CoordinationJournal`.
393+
394+
## Result Evaluation
395+
396+
Plug in quality assessment via the `ResultEvaluator` SPI. Evaluators run automatically (async, non-blocking, recorded in journal) after each agent call, and can be invoked explicitly:
397+
398+
```java
399+
var result = fleet.agent("writer").call("draft", Map.of("topic", "AI"));
400+
var evals = fleet.evaluate(result, call);
401+
if (evals.stream().allMatch(Evaluation::passed)) {
402+
session.stream(result.text());
403+
}
404+
```
405+
406+
## Long-Term Memory
407+
408+
Agents remember users across sessions. Configuration-only — no code changes in `@Agent` classes:
409+
410+
```java
411+
// LongTermMemoryInterceptor (pre): injects stored facts into system prompt
412+
// LongTermMemoryInterceptor (post): extracts new facts via LLM
413+
414+
// Extraction strategies:
415+
MemoryExtractionStrategy.onSessionClose() // default — batch, cost-efficient
416+
MemoryExtractionStrategy.perMessage() // real-time, expensive
417+
MemoryExtractionStrategy.periodic(5) // every 5 messages
418+
```
419+
420+
Backed by `InMemoryLongTermMemory` (dev) or any `SessionStore` implementation (Redis, SQLite). The `onDisconnect` lifecycle hook ensures facts are extracted before conversation history is cleared.
421+
422+
## Testing
423+
424+
The coordinator module includes test stubs for exercising `@Prompt` methods without infrastructure or LLM calls:
425+
426+
```java
427+
var fleet = StubAgentFleet.builder()
428+
.agent("weather", "Sunny, 72F in Madrid")
429+
.agent("activities", "Visit Retiro Park, Prado Museum")
430+
.build();
431+
432+
coordinator.onPrompt("What to do in Madrid?", fleet, session);
433+
434+
CoordinatorAssertions.assertThat(result)
435+
.succeeded()
436+
.containsText("Madrid")
437+
.completedWithin(Duration.ofSeconds(5));
438+
```
439+
440+
`StubAgentFleet` returns canned responses. `StubAgentTransport` allows predicate-based routing for more complex scenarios.
441+
442+
## Eval Assertions
443+
444+
LLM-as-judge for testing agent response quality. Uses any `AgentRuntime` as the judge model:
445+
446+
```java
447+
var judge = new LlmJudge(cheapRuntime, "gpt-4o-mini");
448+
var response = client.prompt("Should I bring an umbrella to Tokyo?");
449+
450+
assertThat(response)
451+
.withJudge(judge)
452+
.forPrompt("Should I bring an umbrella to Tokyo?")
453+
.meetsIntent("Recommends whether to bring an umbrella based on weather data")
454+
.isGroundedIn("get_weather")
455+
.hasQuality(q -> q.relevance(0.8).coherence(0.7).safety(0.9));
456+
```
457+
458+
See [AI Testing](/docs/reference/testing/) for the full assertion API.
459+
346460
## Console UI
347461

348462
The built-in AI Console renders coordinator activity as **tool cards** — showing each specialist agent's work (tool name, input, result) before displaying the synthesized response. This gives users visibility into the multi-agent orchestration process.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
title: Architecture
3+
description: How Atmosphere's transport and application layers compose
4+
sidebar:
5+
order: 2
6+
---
7+
8+
Atmosphere has two layers. The **transport layer** moves bytes between server and clients. The **application layer** gives those bytes meaning — AI events, tool calls, conversation memory. Understanding how they compose is key to using the framework effectively.
9+
10+
## The Two Layers
11+
12+
```
13+
Application layer StreamingSession, AiEvent, AgentFleet, JournalFormat
14+
|
15+
| emit(), stream(), complete()
16+
|
17+
v
18+
Transport layer Broadcaster, AtmosphereResource, BroadcastFilter
19+
|
20+
| broadcast(), suspend(), write()
21+
|
22+
v
23+
Wire protocols WebSocket, SSE, Long-Polling, gRPC
24+
```
25+
26+
## Broadcaster (Transport)
27+
28+
A **Broadcaster** is a named pub/sub channel. Resources subscribe to it. When you call `broadcaster.broadcast(message)`, every subscribed resource receives it.
29+
30+
```java
31+
@ManagedService(path = "/chat/{room}")
32+
public class Chat {
33+
34+
@Message
35+
public String onMessage(String message) {
36+
return message; // broadcast to all in room
37+
}
38+
}
39+
```
40+
41+
Key properties:
42+
- **1:N** — one message fans out to all subscribers
43+
- **Untyped**`broadcast(Object)` accepts anything
44+
- **Long-lived** — persists across connections, survives reconnects
45+
- **Transport-aware** — BroadcastFilters, BroadcasterCache, clustering (Redis, Kafka)
46+
47+
## StreamingSession (Application)
48+
49+
A **StreamingSession** represents one client's AI conversation. It produces typed events that flow through the Broadcaster to the client.
50+
51+
```java
52+
@AiEndpoint(path = "/ai/chat")
53+
public class MyBot {
54+
55+
@Prompt
56+
public void onPrompt(String message, StreamingSession session) {
57+
session.emit(new AiEvent.ToolStart("search", Map.of("q", message)));
58+
session.emit(new AiEvent.ToolResult("search", results));
59+
session.stream(message); // invoke LLM, stream response tokens
60+
}
61+
}
62+
```
63+
64+
Key properties:
65+
- **1:1** — one session serves one client's conversation
66+
- **Typed**`emit(AiEvent)` produces structured events (tool cards, progress, errors)
67+
- **Short-lived** — created per prompt, closed on completion
68+
- **Application-aware** — conversation memory, LLM runtime, tools, guardrails, metrics
69+
70+
## How They Compose
71+
72+
`StreamingSession` uses a `Broadcaster` internally. When you call `session.emit(event)`, the session serializes the event to JSON and calls `broadcaster.broadcast(new RawMessage(json))`. The Broadcaster delivers it through its filter chain to the client.
73+
74+
```
75+
session.emit(new AiEvent.ToolStart(...))
76+
|
77+
v
78+
DefaultStreamingSession serializes to JSON
79+
|
80+
v
81+
broadcaster.broadcast(new RawMessage(json))
82+
|
83+
v
84+
BroadcastFilter chain (PII redaction, cost metering, content safety)
85+
|
86+
v
87+
AtmosphereResource.write() -> WebSocket frame / SSE event / HTTP chunk
88+
```
89+
90+
This separation is why **AI filters work**. `PiiRedactionFilter` and `CostMeteringFilter` sit on the Broadcaster's filter chain but understand AI event types. They intercept between "session emits event" and "client receives bytes" — a hook point that only exists because the layers are separate.
91+
92+
## API Comparison
93+
94+
| | `Broadcaster` | `StreamingSession` |
95+
|---|---|---|
96+
| Verb | `broadcast()` | `emit()`, `stream()`, `send()` |
97+
| Cardinality | 1:N (topic to subscribers) | 1:1 (conversation to client) |
98+
| Lifetime | Long-lived (across connections) | Per-prompt |
99+
| Type safety | `Object` | `AiEvent` hierarchy |
100+
| State | Resources, filters, cache | Memory, runtime, tools, guardrails |
101+
| Processing hooks | `BroadcastFilter` chain | `AiInterceptor` chain |
102+
103+
## When to Use Which
104+
105+
**Use `Broadcaster` directly** when you're building classic real-time features: chat rooms, dashboards, notifications, collaboration. You're working with raw messages and fan-out.
106+
107+
**Use `StreamingSession`** when you're building AI features: chatbots, agents, tool-calling, multi-agent orchestration. You're working with typed events and conversation state.
108+
109+
**Use both** when you need AI features with custom transport behavior — for example, an AI chatbot in a room where other users see the bot's responses. The session emits events, the Broadcaster fans them out to the room.
110+
111+
## AgentFleet (Orchestration)
112+
113+
A third abstraction sits above `StreamingSession` for multi-agent coordinators:
114+
115+
```
116+
AgentFleet agent(), parallel(), pipeline(), journal()
117+
|
118+
v
119+
StreamingSession emit(), stream(), complete()
120+
|
121+
v
122+
Broadcaster broadcast() -> filters -> resources
123+
```
124+
125+
The `AgentFleet` dispatches work to agents and collects results. It uses the `StreamingSession` to stream progress and results back to the client. The fleet's `CoordinationJournal` records every dispatch and completion for observability.
126+
127+
```java
128+
@Coordinator(name = "ceo",
129+
journalFormat = JournalFormat.Markdown.class)
130+
@Fleet({@AgentRef(type = ResearchAgent.class),
131+
@AgentRef(type = WriterAgent.class)})
132+
public class CeoCoordinator {
133+
134+
@Prompt
135+
public void onPrompt(String message, AgentFleet fleet, StreamingSession session) {
136+
var research = fleet.agent("research").call("search", Map.of("q", message));
137+
session.stream("Summarize: " + research.text());
138+
// journal auto-emitted as a tool card via journalFormat
139+
}
140+
}
141+
```

0 commit comments

Comments
 (0)