Spring Boot Starter
Add persistent, searchable memory to your Spring Boot AI agents in 3 lines of config.
Table of contents
Installation
Maven
<dependency>
<groupId>dev.hippodid</groupId>
<artifactId>hippodid-spring-boot-starter</artifactId>
<version>0.1.0</version>
</dependency>
Gradle
implementation 'dev.hippodid:hippodid-spring-boot-starter:0.1.0'
Requires Java 21+ and Spring Boot 3.3+.
Configuration reference
Add to your application.yml:
hippodid:
api-key: hd_key_your_key_here # Required — get yours at hippodid.com
character-id: your-character-uuid # Optional — default character ID
base-url: https://api.hippodid.com # Optional — default shown
| Property | Required | Default | Description |
|---|---|---|---|
hippodid.api-key | Yes | — | Your HippoDid API key (hd_key_...) |
hippodid.character-id | No | — | Default character for defaultCharacter() |
hippodid.base-url | No | https://api.hippodid.com | API base URL (override for local dev) |
The starter activates automatically when hippodid.api-key is present. No @Enable annotation needed.
Auto-configured beans
When hippodid.api-key is set, the starter creates:
| Bean | Type | Condition |
|---|---|---|
hippoDidClient | HippoDidClient | Always (when api-key present) |
hippoDidHealthIndicator | HippoDidHealthIndicator | When spring-boot-starter-actuator is on classpath |
To disable auto-configuration:
spring:
autoconfigure:
exclude: dev.hippodid.autoconfigure.HippoDidAutoConfiguration
Injecting the client
@Service
public class AgentMemoryService {
private final HippoDidClient hippodid;
public AgentMemoryService(HippoDidClient hippodid) {
this.hippodid = hippodid;
}
public void remember(String agentId, String observation) {
hippodid.characters(agentId).memories().add(observation);
}
public List<MemoryResult> recall(String agentId, String query) {
return hippodid
.characters(agentId)
.search(query, SearchOptions.defaults())
.memories();
}
}
HippoDidClient fluent API
Tenant-level: hippodid.characters()
// Create a character
CharacterInfo agent = hippodid.characters()
.create("My Agent", "Personal AI assistant");
// List all characters
List<CharacterInfo> all = hippodid.characters().list();
Character-scoped: hippodid.characters(id)
CharacterHandle handle = hippodid.characters("your-character-uuid");
Semantic search
// Search with defaults (topK=10, all categories)
List<MemoryResult> results = handle
.search("user UI preferences", SearchOptions.defaults())
.memories();
// Search with custom options
List<MemoryResult> results = handle
.search("technical choices",
SearchOptions.builder()
.topK(5)
.categories(List.of("decisions", "skills"))
.build())
.memories();
Each MemoryResult has:
memoryId()— the memory UUIDcontent()— the memory textcategory()— the category (e.g.,"preferences","decisions")relevanceScore()— semantic similarity [0.0, 1.0]finalScore()— combined score after salience × decay weighting
Add memories
// AI extraction — AUDN pipeline extracts structured memory (Starter+ tier)
MemoryInfo mem = handle.memories()
.add("User prefers dark mode and vim keybindings");
// With source type hint
MemoryInfo mem = handle.memories()
.add("We decided to use PostgreSQL over MySQL", "meeting");
// Direct write — stored exactly as provided (Starter+ tier, no AI)
MemoryInfo mem = handle.memories()
.addDirect(
"Prefers Go over Java for new backend services",
"decisions",
0.9 // salience [0.0, 1.0]
);
Export
// Export all memories to a local Markdown file
Path file = handle.export(ExportFormat.MARKDOWN, Path.of("agent-memory.md"));
// Export as JSON
Path file = handle.export(ExportFormat.JSON, Path.of("agent-memory.json"));
Default character shortcut
When hippodid.character-id is configured, use defaultCharacter():
hippodid.defaultCharacter().memories().add("Observation here");
Tier information
TierInfo tier = hippodid.tier();
log.info("Tier: {}, characters: {}/{}",
tier.tier(), tier.currentCharacterCount(), tier.maxCharacters());
Full working example
A Spring Boot application that gives an AI agent persistent memory:
@SpringBootApplication
public class AgentApplication {
public static void main(String[] args) {
SpringApplication.run(AgentApplication.class, args);
}
}
@RestController
@RequestMapping("/agent")
public class AgentController {
private final HippoDidClient hippodid;
private final String characterId = "your-character-uuid";
public AgentController(HippoDidClient hippodid) {
this.hippodid = hippodid;
}
@PostMapping("/observe")
public ResponseEntity<Void> observe(@RequestBody ObserveRequest req) {
hippodid.characters(characterId)
.memories()
.add(req.observation());
return ResponseEntity.ok().build();
}
@GetMapping("/recall")
public ResponseEntity<List<MemoryResult>> recall(@RequestParam String query) {
List<MemoryResult> memories = hippodid
.characters(characterId)
.search(query, SearchOptions.defaults())
.memories();
return ResponseEntity.ok(memories);
}
}
record ObserveRequest(String observation) {}
# application.yml
hippodid:
api-key: ${HIPPODID_API_KEY}
character-id: ${HIPPODID_CHARACTER_ID}
HippoDidHealthIndicator
When spring-boot-starter-actuator is on the classpath, the starter registers a health indicator at GET /actuator/health/hippoDid:
{
"status": "UP",
"components": {
"hippoDid": {
"status": "UP",
"details": {
"tier": "DEVELOPER",
"characters": "5/30",
"aiExtraction": true,
"teamSharing": true,
"baseUrl": "https://api.hippodid.com"
}
}
}
}
When the API key is invalid or the API is unreachable:
{
"hippoDid": {
"status": "DOWN",
"details": {
"error": "[401] Unauthorized: Invalid API key",
"statusCode": 401
}
}
}
To customize the health indicator, define your own bean:
@Bean
public HippoDidHealthIndicator hippoDidHealthIndicator(HippoDidClient client) {
return new HippoDidHealthIndicator(client); // extend or wrap as needed
}
ClerkTenantResolver (multi-tenant apps)
For applications using Clerk for authentication, ClerkTenantResolver extracts the Clerk organization ID from the user’s JWT. Useful for multi-tenant apps where each organization is a separate HippoDid tenant.
Add the bean manually (not auto-created):
@Bean
public ClerkTenantResolver clerkTenantResolver() {
return new ClerkTenantResolver();
}
Use in a controller or filter:
@PostMapping("/memories")
public ResponseEntity<Void> addMemory(
@RequestHeader("Authorization") String authHeader,
@RequestBody MemoryRequest req) {
String token = authHeader.replace("Bearer ", "");
String tenantId = clerkTenantResolver
.resolveTenantId(token) // org_id if org session, user_id otherwise
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED));
hippodid.characters(req.characterId())
.memories()
.add(req.content());
return ResponseEntity.ok().build();
}
ClerkTenantResolver methods:
| Method | Returns | Description |
|---|---|---|
resolveOrgId(token) | Optional<String> | Clerk org_id (org session) |
resolveUserId(token) | Optional<String> | Clerk user sub (any session) |
resolveTenantId(token) | Optional<String> | org_id if present, otherwise sub |
ClerkTenantResolver reads JWT claims only — it does not validate the JWT signature. Validate the signature upstream (e.g., a Spring Security filter or API gateway) before trusting the claims.
Error handling
All client methods throw HippoDidException (unchecked) on API errors:
try {
hippodid.characters().create("Agent", null);
} catch (HippoDidException e) {
log.error("HippoDid error {}: {} — {}",
e.statusCode(), e.errorType(), e.getMessage());
}
Common error types:
errorType() | HTTP | When |
|---|---|---|
Unauthorized | 401 | Invalid or missing API key |
CharacterNotFound | 404 | Character UUID does not exist |
MemoryNotFound | 404 | Memory UUID does not exist |
CharacterLimitExceeded | 429 | Tier’s max character limit reached |
TierLimitExceeded | 429 | Rate limit or AI ops quota exceeded |
TierFeatureNotAvailable | 403 | Feature not available on current tier |
Testing
MockWebServer (recommended)
@BeforeEach
void setUp() throws IOException {
mockServer = new MockWebServer();
mockServer.start();
HippoDidProperties props = new HippoDidProperties();
props.setApiKey("hd_key_test");
props.setBaseUrl(mockServer.url("/").toString());
WebClient webClient = WebClient.builder()
.baseUrl(props.getBaseUrl())
.defaultHeader("Authorization", "Bearer " + props.getApiKey())
.build();
client = new HippoDidClient(props, webClient);
}
@Test
void memoryIsStored() throws Exception {
mockServer.enqueue(new MockResponse()
.setResponseCode(201)
.setBody("{\"id\":\"mem1\",\"content\":\"Dark mode\",\"category\":\"preferences\"," +
"\"salience\":0.8,\"state\":\"ACTIVE\"," +
"\"createdAt\":\"2024-01-01T00:00:00Z\",\"updatedAt\":\"2024-01-01T00:00:00Z\"," +
"\"characterId\":\"char1\"}")
.addHeader("Content-Type", "application/json"));
MemoryInfo mem = client.characters("char1")
.memories()
.addDirect("Dark mode", "preferences", 0.8);
assertThat(mem.category()).isEqualTo("preferences");
}
Add to test dependencies:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.12.0</version>
<scope>test</scope>
</dependency>
Spring Boot test slice
@SpringBootTest
@TestPropertySource(properties = {
"hippodid.api-key=hd_key_test",
"hippodid.base-url=http://localhost:${mockserver.port}"
})
class AgentServiceTest {
@Autowired
HippoDidClient hippodid;
// ...
}