Skip to content

Commit 091ce34

Browse files
committed
docs(reference/ai): add approval gates, compaction SPI, artifact SPI, TCK, event model
New sections: approval wire protocol and ADK bridge, AiCompactionStrategy with sliding window and summarizing, ArtifactStore SPI with defensive copies, interceptor disconnect lifecycle, AiEvent normalization matrix, tiered capability matrix, cross-runtime TCK
1 parent 608ef6b commit 091ce34

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

  • docs/src/content/docs/reference

docs/src/content/docs/reference/ai.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,234 @@ Configure the built-in client with environment variables:
365365
| `ConversationPersistence` | SPI for durable conversation storage (Redis, SQLite) |
366366
| `RetryPolicy` | Exponential backoff with circuit-breaker semantics |
367367

368+
## Approval Gates
369+
370+
`@RequiresApproval` pauses tool execution until the client approves. The virtual thread parks cheaply on a `CompletableFuture` -- no carrier thread consumed.
371+
372+
```java
373+
@AiTool(name = "delete_account", description = "Permanently delete a user account")
374+
@RequiresApproval("This will permanently delete the account. Are you sure?")
375+
public String deleteAccount(@Param("accountId") String accountId) {
376+
return accountService.delete(accountId);
377+
}
378+
```
379+
380+
### Wire Protocol
381+
382+
When the LLM calls a `@RequiresApproval` tool, the client receives an `approval-required` event:
383+
384+
```json
385+
{"event":"approval-required","data":{
386+
"approvalId":"apr_a1b2c3d4e5f6",
387+
"toolName":"delete_account",
388+
"arguments":{"accountId":"user-42"},
389+
"message":"This will permanently delete the account. Are you sure?",
390+
"expiresIn":300
391+
}}
392+
```
393+
394+
The client responds with:
395+
- `/__approval/apr_a1b2c3d4e5f6/approve` -- tool executes
396+
- `/__approval/apr_a1b2c3d4e5f6/deny` -- tool returns cancelled
397+
398+
Default timeout: 5 minutes. Configurable via `@RequiresApproval(timeoutSeconds = 120)`.
399+
400+
### How It Works
401+
402+
1. `AiStreamingSession.wrapApprovalGates()` wraps `@RequiresApproval` tools with `ApprovalGateExecutor`
403+
2. When the LLM calls the tool, `ApprovalGateExecutor` parks the virtual thread on `CompletableFuture.get(timeout)`
404+
3. The session emits `AiEvent.ApprovalRequired` to the client
405+
4. `AiEndpointHandler` fast-paths `/__approval/` messages to the session's `ApprovalRegistry` (before prompt dispatch)
406+
5. `ApprovalRegistry.tryResolve()` completes the future, unparking the virtual thread
407+
6. On transport reconnect, a fallback scan across all active sessions ensures the approval reaches the parked thread
408+
409+
### ADK ToolConfirmation Bridge
410+
411+
When running on Google ADK, Atmosphere also calls `toolContext.requestConfirmation()` to give ADK native visibility into the approval pause. If ADK resolves a confirmation before Atmosphere (e.g., via its own UI), the ADK denial short-circuits without calling the executor. This creates a two-layer model: Atmosphere-level (cross-runtime) + ADK-native (runtime-specific).
412+
413+
ADK runtimes declare `AiCapability.TOOL_APPROVAL`.
414+
415+
## Context Compaction SPI
416+
417+
The `AiCompactionStrategy` SPI controls how conversation history is compacted when it exceeds the configured limit. Unlike `MemoryStrategy` (which selects messages for the next request -- read path), compaction permanently reduces stored history (write path).
418+
419+
```java
420+
public interface AiCompactionStrategy {
421+
List<ChatMessage> compact(List<ChatMessage> messages, int maxMessages);
422+
String name();
423+
}
424+
```
425+
426+
### Built-in Strategies
427+
428+
**`SlidingWindowCompaction`** (default) -- drops the oldest non-system messages until under the limit. System messages are always preserved.
429+
430+
**`SummarizingCompaction`** -- condenses old messages into a single system-role summary, preserving the most recent messages verbatim. The recent window size is configurable (default: 6).
431+
432+
```java
433+
// Default: sliding window
434+
var memory = new InMemoryConversationMemory(20);
435+
436+
// Custom: summarization with 8-message recent window
437+
var memory = new InMemoryConversationMemory(20, new SummarizingCompaction(8));
438+
```
439+
440+
### ADK Bridge
441+
442+
`AdkCompactionBridge.toAdkConfig()` maps Atmosphere compaction settings to ADK's `EventsCompactionConfig` for native compaction when using the ADK runtime.
443+
444+
## Artifact Persistence SPI
445+
446+
The `ArtifactStore` SPI provides binary artifact persistence across agent runs. Use cases include agent-generated reports, images, code files, and content shared between coordinated agents.
447+
448+
```java
449+
public interface ArtifactStore {
450+
Artifact save(Artifact artifact); // auto-versions
451+
Optional<Artifact> load(String namespace, String artifactId); // latest version
452+
Optional<Artifact> load(String namespace, String artifactId, int version);
453+
List<Artifact> list(String namespace); // latest of each
454+
boolean delete(String namespace, String artifactId); // all versions
455+
void deleteAll(String namespace);
456+
}
457+
```
458+
459+
### Artifact Record
460+
461+
```java
462+
public record Artifact(
463+
String id, // unique identifier
464+
String namespace, // grouping key (session ID, agent name, user ID)
465+
String fileName, // human-readable name ("report.pdf")
466+
String mimeType, // MIME type ("application/pdf")
467+
byte[] data, // binary content (defensively copied)
468+
int version, // auto-incremented per save
469+
Map<String, String> metadata, // arbitrary key-value pairs
470+
Instant createdAt
471+
) { }
472+
```
473+
474+
Byte arrays are defensively copied on construction and on access -- callers cannot mutate persisted data.
475+
476+
### Implementations
477+
478+
- **`InMemoryArtifactStore`** -- default, for development and testing. Data does not survive JVM restart.
479+
- **ADK bridge** -- `AdkArtifactBridge.toAdkService()` wraps an `ArtifactStore` as ADK's `BaseArtifactService`.
480+
481+
## Interceptor Disconnect Lifecycle
482+
483+
`AiInterceptor` includes an `onDisconnect` hook called **before** conversation memory is cleared. This enables fact extraction, summary persistence, and other cleanup that requires access to the conversation history.
484+
485+
```java
486+
public interface AiInterceptor {
487+
default AiRequest preProcess(AiRequest request, AtmosphereResource resource) { return request; }
488+
default void postProcess(AiRequest request, AtmosphereResource resource) { }
489+
default void onDisconnect(String userId, String conversationId, List<ChatMessage> history) { }
490+
}
491+
```
492+
493+
`LongTermMemoryInterceptor.onDisconnect()` uses this to extract facts from the full conversation on session close via `OnSessionCloseStrategy`.
494+
495+
Execution order: `preProcess` runs FIFO, `postProcess` runs LIFO, `onDisconnect` runs FIFO. Exceptions in one interceptor do not prevent others from being called.
496+
497+
## AiEvent Model
498+
499+
The `AiEvent` sealed interface provides 15 structured event types emitted via `session.emit()`. All runtimes map their native events to this common model.
500+
501+
| Event | Description |
502+
|-------|-------------|
503+
| `TextDelta` | Streaming token |
504+
| `TextComplete` | Final assembled text |
505+
| `ToolStart` | Tool invocation begins (name + arguments) |
506+
| `ToolResult` | Tool executed successfully (name + result) |
507+
| `ToolError` | Tool execution failed |
508+
| `AgentStep` | Orchestration step (ADK agent steps, Embabel planning) |
509+
| `StructuredField` | Structured output field arrival |
510+
| `EntityStart` / `EntityComplete` | Structured entity streaming |
511+
| `RoutingDecision` | Backend routing event |
512+
| `Progress` | Long-running operation status |
513+
| `Handoff` | Agent handoff notification |
514+
| `ApprovalRequired` | Human approval gate |
515+
| `Error` | Error with recovery hint |
516+
| `Complete` | Stream completed with usage metadata |
517+
518+
### Runtime Event Normalization
519+
520+
| Source | Atmosphere Event |
521+
|--------|-----------------|
522+
| ADK `event.functionCalls()` | `AiEvent.ToolStart` |
523+
| ADK `event.functionResponses()` | `AiEvent.ToolResult` |
524+
| ADK `event.author()` (non-partial) | `AiEvent.AgentStep` |
525+
| ADK `event.usageMetadata()` | `ai.tokens.input/output/total` metadata |
526+
| Koog `onToolCallStarting` | `AiEvent.ToolStart` |
527+
| Koog `onToolCallCompleted` | `AiEvent.ToolResult` |
528+
| Koog `onToolCallFailed` | `AiEvent.ToolError` |
529+
| Koog `StreamFrame.ReasoningDelta` | `AiEvent.Progress` |
530+
| Embabel `MessageOutputChannelEvent` | `AiEvent.TextDelta` |
531+
| Embabel `ProgressOutputChannelEvent` | `AiEvent.AgentStep` |
532+
533+
## Capability Matrix
534+
535+
Each runtime declares capabilities via `AiCapability`. The framework uses these for model routing, tool negotiation, and feature discovery.
536+
537+
### Guaranteed by Core
538+
539+
Available on **all** runtimes:
540+
541+
| Capability | Description |
542+
|-----------|-------------|
543+
| `TEXT_STREAMING` | Basic text streaming |
544+
| `SYSTEM_PROMPT` | System prompt support |
545+
546+
### Runtime-Dependent
547+
548+
| Capability | Built-in | LangChain4j | Spring AI | ADK | Embabel | Koog |
549+
|-----------|----------|-------------|-----------|-----|---------|------|
550+
| `TOOL_CALLING` | Y | Y | Y | Y | | Y |
551+
| `STRUCTURED_OUTPUT` | Y | Y | Y | Y | Y | Y |
552+
| `CONVERSATION_MEMORY` | | | | Y | | Y |
553+
554+
### Experimental
555+
556+
| Capability | Built-in | LangChain4j | Spring AI | ADK | Embabel | Koog |
557+
|-----------|----------|-------------|-----------|-----|---------|------|
558+
| `AGENT_ORCHESTRATION` | | | | Y | Y | Y |
559+
| `TOOL_APPROVAL` | | | | Y | | |
560+
| `VISION` | | | | | | |
561+
| `AUDIO` | | | | | | |
562+
563+
## Cross-Runtime Contract Tests (TCK)
564+
565+
The `AbstractAgentRuntimeContractTest` base class in `atmosphere-ai-test` enforces a minimum contract across all runtime adapters.
566+
567+
```java
568+
public abstract class AbstractAgentRuntimeContractTest {
569+
protected abstract AgentRuntime createRuntime();
570+
protected abstract AgentExecutionContext createTextContext();
571+
protected abstract AgentExecutionContext createToolCallContext();
572+
protected abstract AgentExecutionContext createErrorContext();
573+
574+
// Enforced contracts:
575+
// - runtimeDeclaresMinimumCapabilities (TEXT_STREAMING)
576+
// - runtimeHasNonBlankName
577+
// - runtimeIsAvailable
578+
// - textStreamingCompletesSession (10s timeout)
579+
// - toolCallExecutesIfSupported
580+
// - errorContextTriggersSessionError
581+
}
582+
```
583+
584+
Add `atmosphere-ai-test` as a test dependency and extend the base class:
585+
586+
```xml
587+
<dependency>
588+
<groupId>org.atmosphere</groupId>
589+
<artifactId>atmosphere-ai-test</artifactId>
590+
<scope>test</scope>
591+
</dependency>
592+
```
593+
594+
The `RecordingSession` test double captures all events, text chunks, metadata, and errors for assertion. Currently enforced on: ADK, LangChain4j, Spring AI.
595+
368596
## Samples
369597

370598
- [Spring Boot AI Chat](../samples/spring-boot-ai-chat/) -- built-in client with Gemini/OpenAI/Ollama

0 commit comments

Comments
 (0)