Spring Boot Starter

Add persistent, searchable memory to your Spring Boot AI agents in 3 lines of config.

Table of contents
  1. Installation
    1. Maven
    2. Gradle
  2. Configuration reference
  3. Auto-configured beans
  4. Injecting the client
  5. HippoDidClient fluent API
    1. Tenant-level: hippodid.characters()
    2. Character-scoped: hippodid.characters(id)
      1. Semantic search
      2. Add memories
      3. Export
    3. Default character shortcut
    4. Tier information
  6. Full working example
  7. HippoDidHealthIndicator
  8. ClerkTenantResolver (multi-tenant apps)
  9. Error handling
  10. Testing
    1. MockWebServer (recommended)
    2. Spring Boot test slice
  11. Links

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");
// 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 UUID
  • content() — the memory text
  • category() — 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

@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;

    // ...
}


Copyright © 2024 HippoDid. Distributed under the MIT License.

This site uses Just the Docs, a documentation theme for Jekyll.