shikumi-cache 0.2.0.0 → 0.2.0.1
raw patch · 9 files changed
+102/−89 lines, 9 filesdep ~baikaidep ~effectfuldep ~shikumi-cachePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: baikai, effectful, shikumi-cache
API changes (from Hackage documentation)
Files
- CHANGELOG.md +4/−0
- shikumi-cache.cabal +65/−56
- src/Shikumi/Cache.hs +2/−2
- src/Shikumi/Cache/Backend/Memory.hs +1/−1
- src/Shikumi/Cache/Backend/SQLite.hs +6/−6
- src/Shikumi/Cache/Key.hs +4/−4
- src/Shikumi/Cache/ResponseJSON.hs +18/−18
- src/Shikumi/Cache/Types.hs +1/−1
- test/Main.hs +1/−1
CHANGELOG.md view
@@ -2,6 +2,10 @@ ## Unreleased +## 0.2.0.1 — 2026-10-05++- Move the dependency on `mori://shinzui/baikai/packages/baikai` to `>=0.7.1.0 && <0.8` and widen the `effectful` bound to `>=2.6 && <2.8`, so both effectful 2.6 and 2.7 are supported (effectful 2.7 needs `baikai-effectful` 0.4.0.2, effectful 2.6 needs 0.4.0.1). Bounds only; no source changed.+ ## 0.2.0.0 — 2026-09-08 - Raise the internal `shikumi` bound to `^>=0.4.0.0` for the breaking core release.
shikumi-cache.cabal view
@@ -1,8 +1,8 @@-cabal-version: 3.4-name: shikumi-cache-version: 0.2.0.0-synopsis: Content-addressed response caching for shikumi (EP-6)-category: AI+cabal-version: 3.4+name: shikumi-cache+version: 0.2.0.1+synopsis: Content-addressed response caching for shikumi (EP-6)+category: AI description: The caching subsystem for shikumi: a content-addressed cache key (a BLAKE3 digest over a canonical request serialization — the contract EP-7's replay@@ -10,20 +10,26 @@ @cachedLLM@ memoizing interpreter that wraps EP-1's @LLM@ effect so identical requests contact the provider once. -license: BSD-3-Clause-author: Nadeem Bitar-maintainer: nadeem@gmail.com-build-type: Simple+license: BSD-3-Clause+author: Nadeem Bitar+maintainer: nadeem@gmail.com+build-type: Simple extra-doc-files: CHANGELOG.md common common-options ghc-options:- -Wall -Wcompat -Widentities -Wincomplete-uni-patterns- -Wincomplete-record-updates -Wredundant-constraints- -fhide-source-paths -Wmissing-export-lists -Wpartial-fields+ -Wall+ -Wcompat+ -Widentities+ -Wincomplete-uni-patterns+ -Wincomplete-record-updates+ -Wredundant-constraints+ -fhide-source-paths+ -Wmissing-export-lists+ -Wpartial-fields -Wmissing-deriving-strategies - default-language: GHC2024+ default-language: GHC2024 default-extensions: DeriveAnyClass DuplicateRecordFields@@ -32,8 +38,8 @@ OverloadedStrings library- import: common-options- hs-source-dirs: src+ import: common-options+ hs-source-dirs: src exposed-modules: Shikumi.Cache Shikumi.Cache.Backend.Effort@@ -44,48 +50,51 @@ Shikumi.Cache.Types build-depends:- , aeson >=2.2 && <2.3- , baikai >=0.7.0.0 && <0.8- , base >=4.20 && <5- , blake3 >=0.3 && <0.4- , bytestring >=0.11 && <0.13- , containers >=0.6 && <0.9- , direct-sqlite >=2.3 && <2.4- , effectful >=2.5 && <2.7- , generic-lens >=2.2 && <2.4- , lens ^>=5.3- , scientific >=0.3 && <0.4- , shikumi ^>=0.4.0.0- , stm >=2.5 && <2.6- , text ^>=2.1- , time >=1.12 && <1.17- , vector >=0.13 && <0.14+ aeson >=2.2 && <2.3,+ baikai >=0.7.1.0 && <0.8,+ base >=4.20 && <5,+ blake3 >=0.3 && <0.4,+ bytestring >=0.11 && <0.13,+ containers >=0.6 && <0.9,+ direct-sqlite >=2.3 && <2.4,+ effectful >=2.6 && <2.8,+ generic-lens >=2.2 && <2.4,+ lens ^>=5.3,+ scientific >=0.3 && <0.4,+ shikumi ^>=0.4.0.0,+ stm >=2.5 && <2.6,+ text ^>=2.1,+ time >=1.12 && <1.17,+ vector >=0.13 && <0.14, test-suite shikumi-cache-test- import: common-options- type: exitcode-stdio-1.0+ import: common-options+ type: exitcode-stdio-1.0 hs-source-dirs: test- main-is: Main.hs- ghc-options: -threaded -with-rtsopts=-N+ main-is: Main.hs+ ghc-options:+ -threaded+ -with-rtsopts=-N+ build-depends:- , aeson- , baikai >=0.7.0.0 && <0.8- , base- , bytestring- , containers- , direct-sqlite- , directory- , effectful- , filepath- , generic-lens- , lens- , process- , shikumi ^>=0.4.0.0- , shikumi-cache ^>=0.2.0.0- , stm- , tasty- , tasty-hunit- , temporary- , text- , time- , vector+ aeson,+ baikai >=0.7.1.0 && <0.8,+ base,+ bytestring,+ containers,+ direct-sqlite,+ directory,+ effectful,+ filepath,+ generic-lens,+ lens,+ process,+ shikumi ^>=0.4.0.0,+ shikumi-cache ^>=0.2.0.1,+ stm,+ tasty,+ tasty-hunit,+ temporary,+ text,+ time,+ vector,
src/Shikumi/Cache.hs view
@@ -64,7 +64,7 @@ -- | Policy knobs for 'cachedLLMWith', shared by every backend. -- -- 'entryTTL' is the maximum age of a usable entry, measured against--- 'CachedResponse.storedAt' at lookup time. 'Nothing' (the default) means+-- 'Shikumi.Cache.Types.storedAt' at lookup time. 'Nothing' (the default) means -- entries never expire, which is the uniform default across Memory, SQLite, -- Redis, and Postgres. Expiry is enforced here, at the policy layer, so it -- behaves identically no matter which backend interprets the 'Cache' effect; an@@ -97,7 +97,7 @@ Eff es a cachedLLM = cachedLLMWith defaultCacheConfig --- | A configured variant of 'cachedLLM'. See 'CacheConfig' for the shared TTL+-- | A configured variant of 'cachedLLM'. See t'CacheConfig' for the shared TTL -- policy. cachedLLMWith :: (Cache :> es, LLM :> es, Time :> es, Error ShikumiError :> es) =>
src/Shikumi/Cache/Backend/Memory.hs view
@@ -1,6 +1,6 @@ -- | The in-memory cache backend (EP-6): a process-local @STM@ map from the hex -- cache key to a 'CachedResponse'. It stores the Haskell value directly (no--- serialization), so it needs no JSON instances for the baikai 'Response' graph.+-- serialization), so it needs no JSON instances for the baikai t'Baikai.Response.Response' graph. -- Build the store with 'newMemoryCache' and discharge the 'Cache' effect with -- 'runCacheMemory'. module Shikumi.Cache.Backend.Memory
src/Shikumi/Cache/Backend/SQLite.hs view
@@ -1,15 +1,15 @@ -- | The SQLite cache backend (EP-6) — a durable, embedded, single-file store. ----- Unlike the in-memory backend (which holds the Haskell 'CachedResponse'+-- Unlike the in-memory backend (which holds the Haskell t'CachedResponse' -- directly), SQLite persists the entry as JSON in a file, so a cache written by -- one process is read back by a later process: run a program, kill it, restart, -- and an identical request is served from disk with no provider call. The JSON -- round-trip for baikai's 'Baikai.Response.Response' graph comes from--- "Shikumi.Cache.ResponseJSON" (re-exported through 'CachedResponse''s--- 'Data.Aeson.ToJSON'/'Data.Aeson.FromJSON').+-- "Shikumi.Cache.ResponseJSON" (re-exported through t'CachedResponse'\'s+-- t'Data.Aeson.ToJSON'/t'Data.Aeson.FromJSON'). -- -- The store is a thin embedded SQLite database (no server) accessed via--- @direct-sqlite@. A single 'Database.SQLite3.Database' handle is guarded by an+-- @direct-sqlite@. A single t'Database.SQLite3.Database' handle is guarded by an -- 'MVar' so the 'Cache' effect's lookups and stores are serialized (SQLite's -- default threading mode does not allow concurrent use of one connection). WAL -- mode and a busy timeout make separate processes cooperate better. Lookup and@@ -43,13 +43,13 @@ import Shikumi.Cache (Cache (..), CacheKey (unCacheKey), CachedResponse (..)) import Shikumi.Cache.Backend.Effort (bestEffortIO) --- | A handle to an open SQLite-backed cache. The 'Database' is behind an 'MVar'+-- | A handle to an open SQLite-backed cache. The t'Database' is behind an 'MVar' -- so all access through the 'Cache' effect is serialized. newtype SQLiteCache = SQLiteCache {db :: MVar Database} -- | The schema. The @key@ is the 64-hex 'CacheKey' (already version-namespaced -- via the @version@ field baked into the hash). @value@ is the UTF-8 JSON of--- 'CachedResponse'. @stored_at@ is ISO-8601 for inspection; policy-layer TTL+-- t'CachedResponse'. @stored_at@ is ISO-8601 for inspection; policy-layer TTL -- uses the same timestamp inside the JSON value. createTableSQL :: Text createTableSQL =
src/Shikumi/Cache/Key.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE DataKinds #-} -- | The content-addressed cache key (EP-6) — the MasterPlan's integration point--- #7. A 'CacheKey' is a BLAKE3 256-bit digest (64 lowercase hex chars) over a+-- #7. A t'CacheKey' is a BLAKE3 256-bit digest (64 lowercase hex chars) over a -- /canonical/ JSON serialization of everything about a request that can change -- the model's answer: the model routing identity including base URL, model -- default headers, and compat shim; per-call headers; the rendered prompt with@@ -15,7 +15,7 @@ -- 'currentKeyVersion'; a bump invalidates all cache entries and makes previously -- recorded @shikumi-trace@ files unreplayable, with replay failing closed via -- @ReplayDivergence@. The replay engine in--- @docs/plans/7-hierarchical-tracing-observability-and-replay.md@ (EP-7) reuses+-- @docs\/plans\/7-hierarchical-tracing-observability-and-replay.md@ (EP-7) reuses -- 'cacheKey' verbatim, so both plans agree byte-for-byte; the golden test in the -- test suite pins the exact hex for a fixed request to catch any drift. module Shikumi.Cache.Key@@ -48,7 +48,7 @@ import Data.Text qualified as T -- | The current cache key-namespace version. Baked into every hashed request--- (the @version@ field) and into 'Shikumi.Cache.Types.CachedResponse.keyVersion'.+-- (the @version@ field) and into 'Shikumi.Cache.Types.keyVersion'. -- Bumping it changes every key, making all prior entries unreachable — a clean -- invalidation with no row deletion. Because @shikumi-trace@ stores cache keys -- in trace files and recomputes them during replay, a version bump also makes@@ -113,7 +113,7 @@ toScientific = realToFrac -- | Delete the payload-level @timestamp@ of every message in a serialized--- message vector. baikai's 'Baikai.Message.Message' encodes as+-- message vector. baikai's t'Baikai.Message.Message' encodes as -- @{"tag": ..., "contents": {..., "timestamp": ...}}@; the timestamp records -- when the message value was built, never what the provider sees, so two -- requests differing only in it must share a cache key. Only the
src/Shikumi/Cache/ResponseJSON.hs view
@@ -1,35 +1,35 @@ {-# OPTIONS_GHC -Wno-orphans #-} --- | A faithful JSON round-trip for baikai's 'Response' graph (EP-6).+-- | A faithful JSON round-trip for baikai's t'Response' graph (EP-6). ----- The persistent cache backends (SQLite/Redis/Postgres) store each cached--- 'Baikai.Response.Response' as JSON and read it back. baikai's 'Response' graph--- round-trips only /partially/ out of the box: 'Baikai.Model.Model',--- 'Baikai.Api.Api', 'Baikai.Content.AssistantContent', and--- 'Baikai.StopReason.StopReason' have both 'ToJSON' and 'FromJSON'; but+-- The persistent cache backends (SQLite\/Redis\/Postgres) store each cached+-- 'Baikai.Response.Response' as JSON and read it back. baikai's t'Response' graph+-- round-trips only /partially/ out of the box: t'Baikai.Model.Model',+-- t'Baikai.Api.Api', t'Baikai.Content.AssistantContent', and+-- t'Baikai.StopReason.StopReason' have both t'ToJSON' and t'FromJSON'; but -- 'Baikai.Usage.Usage', 'Baikai.Cost.Cost', and 'Baikai.Cost.CostBreakdown' are--- 'ToJSON'-only, and 'Baikai.Message.AssistantPayload' and 'Response' have no+-- t'ToJSON'-only, and 'Baikai.Message.AssistantPayload' and t'Response' have no -- aeson instances at all. ----- This module supplies the missing pieces as __orphan instances__ so a 'Response'+-- This module supplies the missing pieces as __orphan instances__ so a t'Response' -- can be encoded and decoded losslessly enough for caching. The instances mirror -- baikai's own encoders exactly: ----- * 'Usage' uses @camelTo2 '_'@ field labels (baikai's @usageOptions@), so the--- 'FromJSON' reads the same snake_case keys baikai's 'ToJSON' writes.--- * 'Cost' / 'CostBreakdown' read the @usd@ / @*_usd@ 'Scientific' fields baikai+-- * t'Usage' uses @camelTo2 '_'@ field labels (baikai's @usageOptions@), so the+-- t'FromJSON' reads the same snake_case keys baikai's t'ToJSON' writes.+-- * t'Cost' \/ t'CostBreakdown' read the @usd@ \/ @*_usd@ 'Scientific' fields baikai -- writes (via @fromRationalRepetendUnlimited@) and lift them back to -- 'Rational' with 'toRational'. This is lossy only for non-terminating -- repetends; it never affects the typed-output guarantee, which is decoded--- from the assistant __text__ ('AssistantContent', which round-trips+-- from the assistant __text__ ([AssistantContent]("Baikai.Content#t:AssistantContent"), which round-trips -- exactly) — cost is metadata.--- * 'AssistantPayload' uses @defaultOptions@ (matching baikai's+-- * t'AssistantPayload' uses @defaultOptions@ (matching baikai's -- @deriving anyclass ToJSON@).--- * 'Response' is written out by hand with the same keys @defaultOptions@+-- * t'Response' is written out by hand with the same keys @defaultOptions@ -- produced, minus @evidence@ — see below. ----- __'Response' does not cache its 'Baikai.Evidence.ModelCallEvidence'.__ Since--- baikai 0.5 a 'Response' may carry the evidence record for the call that+-- __t'Response' does not cache its t'Baikai.Evidence.ModelCallEvidence'.__ Since+-- baikai 0.5 a t'Response' may carry the evidence record for the call that -- produced it. That record describes /one/ crossing of the provider boundary; a -- cache hit is precisely the case where no such crossing happened, so replaying -- a stored record would attribute another call's evidence to this one — the@@ -67,7 +67,7 @@ import Data.Aeson.Types (Parser) import Data.Scientific (Scientific) --- | baikai's 'Usage' field-label scheme: @camelTo2 '_'@ (snake_case).+-- | baikai's t'Usage' field-label scheme: @camelTo2 '_'@ (snake_case). usageOptions :: Options usageOptions = defaultOptions {fieldLabelModifier = camelTo2 '_'} @@ -76,7 +76,7 @@ -- with a non-terminating decimal expansion, such as @1 % 3@, does not satisfy -- 'Eq' after an encode/decode round-trip because baikai's encoder drops the -- repetend index. Real USD pricing rates are decimal, so real provider costs--- terminate; do not rely on round-tripped 'CachedResponse' equality for+-- terminate; do not rely on round-tripped 'Shikumi.Cache.Types.CachedResponse' equality for -- synthetic non-decimal costs. ratField :: Object -> Key -> Parser Rational ratField o k = toRational <$> (o .: k :: Parser Scientific)
src/Shikumi/Cache/Types.hs view
@@ -2,7 +2,7 @@ -- operational metadata needed to age and version-guard an entry. -- -- The in-memory backend stores this Haskell value directly (no serialization).--- The persistent backends (SQLite/Redis/Postgres) store its JSON encoding; the+-- The persistent backends (SQLite\/Redis\/Postgres) store its JSON encoding; the -- @ToJSON@/@FromJSON@ for the baikai 'Response' graph that baikai itself does not -- ship in full come from "Shikumi.Cache.ResponseJSON" (imported here for their -- instances). Decoding an entry whose 'keyVersion' does not match the live
test/Main.hs view
@@ -2,7 +2,7 @@ -- golden-pinned), the in-memory backend round-trip, the SQLite backend -- round-trip and cross-process restart durability, the @cachedLLM@ memoizer (the -- headline one-provider-call behaviour) via a counting stub, and--- versioning/invalidation. The server-backed Redis/Postgres backends are tested+-- versioning\/invalidation. The server-backed Redis\/Postgres backends are tested -- in their own packages (@shikumi-cache-redis@/@shikumi-cache-postgres@). -- -- The SQLite restart test re-executes this binary as a subprocess in a "write"