greentic-state

Crates.iogreentic-state
lib.rsgreentic-state
version0.4.2
created_at2025-10-31 12:23:42.895967+00
updated_at2026-01-18 12:29:58.713312+00
descriptionGreentic JSON working-memory store with in-memory and Redis backends
homepage
repositoryhttps://github.com/greentic-ai/greentic-state
max_upload_size
id1909971
size137,709
Greentic - the greener Agentic AI (greentic-ai)

documentation

README

Greentic State

Production-grade JSON working-memory store with pluggable backends for Greentic flows. The crate exposes a StateStore trait that supports whole-document operations as well as targeted updates using JSON Pointer paths. Implementations are provided for an in-memory store (suitable for single-node workers and tests) and Redis (for cross-node coordination).

Design Overview

  • Tenant-aware namespace – Keys are derived from TenantCtx + caller prefix + StateKey, ensuring strict separation between tenants and flow executions.
  • JSON-first API – Values are serde_json::Value with optional JSON Pointer paths for partial reads and writes.
  • TTL semantics – Stores honour per-record TTLs, propagating expirations on updates while allowing TTL refreshes.
  • Bulk operations – Prefix deletion removes all keys under (tenant, prefix) for clean flow teardowns.
  • Feature-gated backends – The redis backend is optional (default feature) and can be disabled for embedded scenarios.
  • Safety guarantees#![forbid(unsafe_code)], lazy expiry in-memory, and Lua-assisted atomic upserts on Redis.

Quickstart

use greentic_state::{inmemory::InMemoryStateStore, StateKey, StatePath, StateStore, TenantCtx};
use greentic_types::{EnvId, TenantId};
use serde_json::json;

let ctx = TenantCtx::new(
    EnvId::try_from("dev").expect("valid env id"),
    TenantId::try_from("tenant-123").expect("valid tenant id"),
);
let prefix = "flow/example";
let key = StateKey::new("node/state");
let store = InMemoryStateStore::new();

// Whole-document write with TTL (in seconds)
store.set_json(&ctx, prefix, &key, None, &json!({"status": "ready"}), Some(300))?;

// Partial update via JSON Pointer
let path = StatePath::from_pointer("/status");
store.set_json(&ctx, prefix, &key, Some(&path), &json!("running"), None)?;

let current = store.get_json(&ctx, prefix, &key, None)?;
assert_eq!(current.unwrap(), json!({"status": "running"}));

Redis backend

use greentic_state::redis_store::RedisStateStore;
use redis::Client;

let client = Client::open("redis://127.0.0.1/")?;
let store = RedisStateStore::new(client);

To run Redis locally:

docker run --rm -p 6379:6379 redis:7
export REDIS_URL=redis://127.0.0.1/

Run tests with Redis enabled:

cargo test --all-features

Tenant Prefixing & FQN

Fully-qualified keys are generated via:

greentic:state:{env}.{tenant_id}[.{team}(.{user})]:{prefix}:{state_key}

Only the generated FQN should be used within backends. StatePath helpers understand a subset of RFC 6901 JSON Pointers (array indices and object keys).

TTL & Expiration

  • In-memory store stores the deadline alongside the value. Expiration is enforced lazily on read/write and during re-insertion.
  • Redis store reuses Redis native TTLs. A Lua upsert script preserves existing TTLs when ttl_secs is None, resets the TTL when a value is provided, and clears TTL when ttl_secs == Some(0).

Partial Updates

set_json with a StatePath performs read-modify-write:

  1. Fetch or create the base JSON document (Value::Null by default).
  2. Navigate/allocate intermediate objects or arrays based on the path segments.
  3. Write the new value at the target pointer, ensuring intermediate containers exist.
  4. Persist the mutated document while preserving TTL semantics.

Bulk Deletion

Use del_prefix to drop all keys under a namespace:

let removed = store.del_prefix(&ctx, "flow/example")?;
println!("Removed {removed} keys");

Redis uses SCAN + batched DEL, avoiding blocking the server on large keyspaces.

Development & CI

  • cargo fmt --all
  • cargo clippy --all-targets -- -D warnings
  • cargo test --workspace --all-features
  • GitHub Actions workflows:
    • auto-tag.yml: tags crates on version bumps merged to master.
    • publish.yml: fmt/clippy/test + idempotent publish via katyo/publish-crates@v2.

Local checks

Run ci/local_check.sh to mirror the main GitHub Actions pipeline before pushing. It handles fmt/clippy/build/tests (spinning up a disposable Redis via Docker when REDIS_URL is unset), dependency gate checks, and an optional cargo publish --dry-run. Useful toggles:

  • LOCAL_CHECK_ONLINE=1 – enable networked steps such as the publish dry run.
  • LOCAL_CHECK_STRICT=1 – fail when optional tools are missing instead of skipping.
  • LOCAL_CHECK_VERBOSE=1 – echo each command (set -x).
  • LOCAL_CHECK_BYPASS=1 – skip the git pre-push hook that calls the script.

By default the script runs offline and skips checks whose tooling is unavailable, matching CI behavior as closely as possible without requiring secrets.

Stability & Maintenance

The crate follows semantic versioning. Publishing is tag-driven and idempotent—rerunning publish on the same version is a no-op. Performance considerations include zero-copy JSON navigation and Redis-side Lua scripts for atomic updates. Contributions should keep shared types in greentic-types and WIT bindings in greentic-interfaces.

Commit count: 17

cargo fmt