Spice Framework v1.0.0
Spice 1.0.0 is the first release built entirely around the new SpiceMessage execution model, a production-grade graph runner, and a refreshed Spring Boot experience (including a dedicated state machine starter). Use this guide to understand what changed, how to configure the new starters, and where future work is headed.
Release Highlightsβ
- Unified execution contract β
SpiceMessage+ExecutionStatenow flow through every graph, checkpoint, and HITL boundary. Legacy Comm/NodeResult types are removed. - DefaultGraphRunner β built-in validation, state transitions, vector cache hydration, and idempotency guards before every node step.
- First-class Spring Boot autoconfiguration β
SpiceAutoConfigurationexposes cache policy, vector cache, idempotency, and event bus wiring behindSpiceFrameworkProperties. - State machine extension β the new
spice-springboot-statemachinemodule automates HITL pause/resume, tool retry/backoff, event emission, metrics, and visualization endpoints. - Reactive Redis persistence β optional Redis-backed idempotency, vector cache, and state machine checkpoints share the same Jedis pool so you can connect once and reuse.
Runtime Architecture Updatesβ
Unified Message + State Modelβ
- The core module consolidates runtime data into
SpiceMessage(spice-core/src/main/kotlin/io/github/noailabs/spice/SpiceMessage.kt) with explicitExecutionStatetransitions and history tracking. All nodes, middleware, and checkpoint helpers now mutate/inspect the same shape. - Tool APIs, registries, and DSL helpers take
Map<String, Any>metadata instead of stringifying values. This keeps retry/error metadata structured when it reaches checkpoint stores or event sinks.
DefaultGraphRunner Enhancementsβ
DefaultGraphRunner (spice-core/.../graph/runner/GraphRunner.kt) is the only implementation most apps need:
- Validates graphs via
SchemaValidationPipelineand enforcesExecutionStateMachineinvariants up front. - Emits graph lifecycle events when
Graph.eventBusis present. The baseline is the in-memory bus, with Redis Streams/Kafka backends planned once those core implementations land (see Known Gaps below). - Integrates idempotency checks by delegating to an
IdempotencyStorebefore each node executes. TTLs and cache windows come fromCachePolicy. - Records vector embeddings for intents/steps when
VectorCacheis supplied (in-memory or Redis). - Understands
WAITINGβRUNNINGresume loops throughresume(...)so HITL responses can be replayed safely.
Checkpoint + HITL Flowβ
GraphRunnerCheckpointExtensions wraps any runner with persistence-aware helpers:
executeWithCheckpointpersistsCheckpointentries whenever a message transitions intoExecutionState.WAITINGor whensaveOnError=true.resumeFromCheckpointloads the serializedSpiceMessage, merges optionalHumanResponse, and moves it back toRUNNINGbefore re-entering the graph.- TTLs and cleanup behavior come from
CheckpointConfig, so you can tune how long HITL sessions remain resumable.
Caching & Idempotencyβ
VectorCacheimplementations (in-memory and Redis) now store typed metadata and calculate cosine similarity inside the module. The Redis implementation shares the Jedis pool with other infrastructure.IdempotencyStoreoffers the same backends and collects hit/miss statistics for observability.CachePolicycentralizes TTL decisions for tool calls, steps, and intents and can be reconfigured via Spring Boot properties.
Spring Boot Starter (spice-springboot)β
SpiceAutoConfiguration replaces the old @EnableSpice annotation and legacy property classes. It exposes modern beans and can be pulled in with:
dependencies {
implementation("io.github.noailabs:spice-springboot")
}
Auto-configured Beansβ
SchemaValidationPipeline,ExecutionStateMachine, andCachePolicyDefaultGraphRunnerwith optionalVectorCache- Conditional
IdempotencyStore(in-memory or Redis) EventBuswith three backends: in-memory (stable), Redis Streams (beta), and Kafka (beta)- Shared
JedisPoolused by idempotency/vector caches and Redis-backed EventBus
Property Referenceβ
| Property | Purpose | Notes |
|---|---|---|
spice.enabled | Global toggle for the starter | defaults to true |
spice.graph-runner.enable-idempotency | Controls idempotency checks | requires an IdempotencyStore bean |
spice.graph-runner.enable-events | Enables EventBus dispatch | Works with all three EventBus backends |
spice.cache.* | TTLs for tool calls, steps, intents | mapped into CachePolicy |
spice.idempotency.* | Backend + namespace configuration | IN_MEMORY or REDIS |
spice.vector-cache.* | Vector cache backend + namespace | shares Redis pool when REDIS |
spice.redis.* | Jedis connection info | reused by all Redis-backed components |
spice.events.enabled | Switches the EventBus bean on/off | stays true for most apps |
spice.events.backend | Selects the EventBus backend (in-memory, redis-streams, kafka) | Redis Streams & Kafka are now first-class options |
spice.events.redis-streams.* | Stream key, consumer prefix, poll timeout, batch size | defaults: spice:events, spice-events, 1s, 100 |
spice.events.kafka.* | Topic, bootstrap servers, poll timeout, client id, ACK settings | defaults: spice.events, localhost:9092, 1s, spice-eventbus, all |
spice.hitl.queue.enabled | Exposes a MessageQueue bean (InMemoryMessageQueue or RedisMessageQueue) | uses shared Jedis pool when backend=redis |
spice.hitl.queue.backend | Queue backend selection (in-memory, redis) | set to redis once you enable spice.redis.* |
spice.hitl.arbiter.enabled | Publishes an Arbiter bean wired to the configured queue | lets apps bootstrap the full HITL worker stack |
Example application.yml snippet:
spice:
graph-runner:
enable-idempotency: true
enable-events: true
redis:
enabled: true
host: redis.prod.local
port: 6379
database: 3
idempotency:
enabled: true
backend: redis
namespace: spice:idempotency:prod
vector-cache:
enabled: true
backend: redis
namespace: spice:vector:prod
EventBus Backendsβ
spice-core now ships RedisStreamsEventBus and KafkaEventBus, both exposed through the starter:
spice:
events:
enabled: true
backend: redis-streams
redis-streams:
stream-key: spice:events
consumer-prefix: spice-events
poll-timeout: 1s
batch-size: 200
spice:
events:
enabled: true
backend: kafka
kafka:
bootstrap-servers: kafka-1:9092,kafka-2:9092
topic: spice.events
client-id: spice-eventbus
poll-timeout: 1s
acks: all
The Redis Streams backend stores every logical topic inside a single stream key and filters client-side (maintaining */** topic patterns). The Kafka backend follows the same envelope format (Spice topic in the record key, serialized SpiceMessage as payload) so existing subscribers keep working regardless of backend choice.
XREADGROUP delivers each entry to only one consumer per group. If you need broadcast-style delivery, either use subscribe(...) (no group) or ensure every consumer registers with its own groupId. Otherwise messages will be load-balanced, not fanned out.
The Kafka backend waits for the broker ACK before returning. If the send fails, the coroutine throws and the caller receives a SpiceResult.Failure, making at-least-once delivery guarantees explicit.
HITL Queue + Arbiter Wiringβ
Two new property blocks make it trivial to stand up the shared queue/arbiter stack:
spice:
redis:
enabled: true
host: redis.internal
hitl:
queue:
enabled: true
backend: redis
namespace: spice:mq:prod
topic: spice.hitl.queue
arbiter:
enabled: true
topic: spice.hitl.queue
spice.hitl.queue.enabled=trueyields aMessageQueuebean (in-memory by default, Redis when requested) using the Jedis pool you already configured.spice.hitl.arbiter.enabled=truecreates anArbiterbean wired to the defaultGraphRunner,ExecutionStateMachine, and optionalDeadLetterHandler. You decide where/how to invokearbiter.start(topic, graphProvider)(ApplicationRunner, coroutine, etc.).- If you bring your own
MessageQueueimplementation, keepspice.hitl.queue.enabled=falseand register a bean manuallyβspice.hitl.arbiter.enabledwill still reuse it.
Spring Boot State Machine Extension (spice-springboot-statemachine)β
Add the module when you need opinionated HITL automation or retry logic:
dependencies {
implementation("io.github.noailabs:spice-springboot-statemachine")
}
What It Providesβ
GraphToStateMachineAdapterthat wraps anyGraphRunnerand drives Spring StateMachine transitions.HitlStateMachineListener+CheckpointSaveActionto persist checkpoints automatically when a graph pauses inExecutionState.WAITING, and to resume when your application publishesHumanResponseEvent.ToolRetryActionwith exponential/fixed/linear backoff strategies configured viaspice.statemachine.retry.NodeExecutionLoggerthat publishesNodeExecutionEventandWorkflowCompletedEventthrough SpringβsApplicationEventPublisherso you can forward them to Kafka, Redis Streams, or Elasticsearch sinks.ReactiveRedisStatePersisterthat serializes extended state as JSON whenspice.statemachine.persistence.type=redis.- Actuator extras:
StateMachineHealthIndicator, Micrometer-backedStateMachineMetrics, and/actuator/statemachine/{visualize|mermaid}endpoints generated byStateMachineVisualizationEndpoint.
Configuration Exampleβ
spice:
statemachine:
enabled: true
persistence:
type: redis
redis:
host: redis.prod.local
port: 6379
key-prefix: spice:sm:
ttl-seconds: 604800
retry:
enabled: true
max-attempts: 3
backoff-strategy: exponential
initial-backoff-ms: 1000
max-backoff-ms: 10000
events:
enabled: true
publish-to-kafka: false # wire custom ApplicationEventListener for Kafka/Redis Streams
visualization:
enabled: true
format: mermaid
When a node transitions into WAITING, checkpoints are persisted, HITL notifications can be emitted, and /actuator/statemachine/mermaid immediately reflects the current topology. When operators respond, publishing a HumanResponseEvent drives SpiceEvent.RESUME without any manual wiring.
Extending Event Deliveryβ
Because events flow through Springβs ApplicationEventPublisher, you can layer your own sinks:
@Component
class RedisStreamSink(private val redisTemplate: ReactiveRedisTemplate<String, String>) {
@EventListener
fun onNodeEvent(event: NodeExecutionEvent) {
redisTemplate.opsForStream<String, String>()
.add("spice.node.execution", mapOf("payload" to json.encodeToString(event)))
.subscribe()
}
}
When spice-core lands Redis Streams or Kafka EventBus implementations, matching property switches in this starter will stand up dedicated beans using the already-configured Jedis pool.
Migration Checklistβ
- Remove legacy
@EnableSpiceannotations; Spring Boot autoconfiguration now registers everything whenspice.enabled=true. - Replace Comm/NodeResult payloads with
SpiceMessage. Tool inputs/outputs now use native Kotlin types inMap<String, Any>. - Adopt
GraphRunner.execute(...)/resume(...)everywhere; node factories and middleware expectSpiceMessage. - Use the new Spring properties for idempotency/vector caches instead of manually instantiating Redis/Jedis clients.
- When HITL support is required, depend on
spice-springboot-statemachineand injectGraphToStateMachineAdapterrather than bolting custom logic toGraphRunnerresponses.
Known Gaps & Follow-upsβ
- EventBus operations tooling β Distributed backends are now available; next steps are richer metrics/health endpoints (lag, consumer status) layered on top of the new EventBus implementations.
- Redis Streams / Kafka queue options β HITL queue wiring currently supports in-memory + Redis List semantics. When we settle on Redis Streams or Kafka queue adapters, wire them under the same
spice.hitl.queue.backendenum. - Documentation versioning β This page lives in the βnextβ docs set. Run
pnpm docusaurus docs:version 1.0.0after locking the release so the 1.0.0 selector appears beside the existing 0.x versions.
Until those pieces land, the new starters are production-ready with in-memory eventing and manual ApplicationEvent sinks, and the state machine module covers the full HITL automation story with Redis-backed persistence.