kioku-core 0.1.0.0 → 0.2.0.0
raw patch · 4 files changed
+377/−18 lines, 4 filesdep ~baikaidep ~baikai-claudedep ~baikai-effectfulPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: baikai, baikai-claude, baikai-effectful, keiki, keiro, keiro-core, kioku-api, kioku-core, kioku-migrations, shikumi, shikumi-trace
API changes (from Hackage documentation)
Files
- CHANGELOG.md +35/−0
- kioku-core.cabal +25/−18
- test/Kioku/CodecCompatSpec.hs +315/−0
- test/Main.hs +2/−0
CHANGELOG.md view
@@ -1,5 +1,40 @@ # Changelog +## 0.2.0.0 — 2026-07-30++### Breaking Changes++- Moved onto the Keiki 0.4 and Keiro 0.4 cohort: `keiki ^>=0.4.0.0`, `keiro ^>=0.4.0.1`, and+ `keiro-core ^>=0.4.0.1`. Kioku no longer builds against Keiki 0.2 or Keiro 0.3, and there is+ no version of `kioku-core` that spans both.+- Keiro 0.4 tightens validated event-stream assembly: `mkEventStreamOrThrow` now rejects a codec+ whose schema version, event tags, or upcaster chain fail `mkCodec`, and it does so at+ evaluation time. Kioku's `memoryEventStream` and `sessionEventStream` both satisfy the stricter+ contract, but a consumer that builds its own streams alongside Kioku's will see the new+ validation at startup.+- A consumer that links `kioku-core` now links Keiro 0.4's API changes directly, including the+ snapshot `state_shape_hash` invalidation and the timer and workflow-children changes. Kioku+ itself calls none of them — both of its event streams set `snapshotPolicy = Never` and+ `stateCodec = Nothing`, and it has no durable workflows — but the types are on the consumer's+ path.+- Moved onto the Baikai 0.4 / Shikumi 0.3.0.1 cohort: `baikai ^>=0.4.1.0`,+ `baikai-claude ^>=0.4.0.1`, `baikai-effectful ^>=0.3.0.2`, `shikumi ^>=0.3.0.1`, and+ `shikumi-trace ^>=0.2.0.1`. The Shikumi bound names an exact patch level deliberately:+ `shikumi-0.3.0.0` is pinned to the Baikai 0.3 series and would let the solver fall off the+ intended cohort silently.++### Changed++- The connection-settings path is unchanged. `Kioku.App.AppEnv.connectionSettings` still carries+ a `Kiroku.Store.Connection.ConnectionSettings` and `runAppIO` still acquires the store resource+ from it; `kiroku-store` does not move major versions in this cohort.++### Added++- Added `Kioku.CodecCompatSpec` to the test-suite: thirteen literal pre-upgrade JSON payloads —+ one per `MemoryEvent` and `SessionEvent` constructor — asserted to decode under the new cohort,+ plus two tests pinning the native and legacy arms of `parseMemoryEvent`'s fallback.+ ## 0.1.0.0 — 2026-07-14 ### Added
kioku-core.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: kioku-core-version: 0.1.0.0+version: 0.2.0.0 synopsis: Reusable agent memory runtime description: Core runtime for kioku. M1 establishes the application effect stack; later@@ -73,11 +73,17 @@ Kioku.Session.ReadModel Kioku.Worker.Failure + -- Two bounds below look wrong but are not. baikai-effectful has no 0.4+ -- release; 0.3.0.2 is its newest and is itself built against baikai ^>=0.4.0,+ -- so its version lags the rest of the Baikai cohort by design. And shikumi is+ -- pinned at ^>=0.3.0.1 rather than ^>=0.3.0.0 because only that patch carries+ -- the baikai 0.4 bounds -- 0.3.0.0 is tied to baikai 0.3 and would give the+ -- solver a way to silently fall off this cohort. build-depends: , aeson >=2.2 && <2.3- , baikai ^>=0.3.0.0- , baikai-claude ^>=0.3.0.0- , baikai-effectful ^>=0.3.0.0+ , baikai ^>=0.4.1.0+ , baikai-claude ^>=0.4.0.1+ , baikai-effectful ^>=0.3.0.2 , base >=4.21 && <5 , bytestring >=0.11 && <0.13 , containers >=0.6 && <0.8@@ -93,17 +99,17 @@ , hasql-pool >=1.2 && <1.5 , hasql-transaction >=1.0 && <1.3 , hs-opentelemetry-api >=1.0 && <1.1- , keiki ^>=0.2.0.0- , keiro ^>=0.3.0.0- , keiro-core ^>=0.3.0.0- , kioku-api ^>=0.1.0.0+ , keiki ^>=0.4.0.0+ , keiro ^>=0.4.0.1+ , keiro-core ^>=0.4.0.1+ , kioku-api ^>=0.2.0.0 , kiroku-store ^>=0.3.0.1 , lens >=5.2 && <5.4 , mmzk-typeid >=0.7 && <0.8 , shibuya-core >=0.8.0.1 && <0.9 , shibuya-kiroku-adapter ^>=0.4.0.0- , shikumi ^>=0.3.0.0- , shikumi-trace ^>=0.2.0.0+ , shikumi ^>=0.3.0.1+ , shikumi-trace ^>=0.2.0.1 , text >=2.1 && <2.2 , time >=1.12 && <1.15 , uuid >=1.3 && <1.4@@ -115,6 +121,7 @@ main-is: Main.hs other-modules: Kioku.AwaitingSpec+ Kioku.CodecCompatSpec Kioku.DistillSpec Kioku.EmbeddingWorkerSpec Kioku.IdempotencySpec@@ -133,7 +140,7 @@ ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends: , aeson >=2.2- , baikai ^>=0.3.0.0+ , baikai ^>=0.4.1.0 , base >=4.21 && <5 , bytestring >=0.11 , containers >=0.6@@ -143,16 +150,16 @@ , effectful-core >=2.5 , hasql >=1.6 , hasql-transaction >=1.0- , keiro ^>=0.3.0.0- , keiro-core ^>=0.3.0.0- , kioku-api ^>=0.1.0.0- , kioku-core ^>=0.1.0.0- , kioku-migrations:test-support ^>=0.1.0.0+ , keiro ^>=0.4.0.1+ , keiro-core ^>=0.4.0.1+ , kioku-api ^>=0.2.0.0+ , kioku-core ^>=0.2.0.0+ , kioku-migrations:test-support ^>=0.2.0.0 , kiroku-store ^>=0.3.0.1 , lens >=5.2 , shibuya-core >=0.8.0.1 && <0.9- , shikumi ^>=0.3.0.0- , shikumi-trace ^>=0.2.0.0+ , shikumi ^>=0.3.0.1+ , shikumi-trace ^>=0.2.0.1 , tasty >=1.5 , tasty-expected-failure >=0.12 , tasty-hunit >=0.10
+ test/Kioku/CodecCompatSpec.hs view
@@ -0,0 +1,315 @@+-- | Pins the on-the-wire shape of every event Kioku persists.+--+-- Keiro 0.4 tightened validated event-stream assembly, so the risk worth holding+-- still is that a payload written by an earlier Kioku stops decoding. Each+-- fixture below is the literal output of the live codec's @encode@ for one+-- constructor, captured from the pre-upgrade tree; the assertions decode them+-- through the same @parseMemoryEvent@ / @parseSessionEvent@ the event store uses+-- and check the round-tripped value field by field.+--+-- 'Kioku.ReiCompatSpec' covers the /legacy/ arm of those parsers -- payloads in+-- the older Rei wire format. This module covers the /native/ arm, and the last+-- test here pins the fallback that joins them, so a future upgrade cannot delete+-- the legacy path without a test going red.+module Kioku.CodecCompatSpec+ ( tests,+ )+where++import Control.Monad ((<=<))+import Data.Aeson (Value, eitherDecode)+import Data.Bifunctor (first)+import Data.ByteString.Lazy (ByteString)+import Data.Set qualified as Set+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Time (UTCTime)+import Data.Time.Format.ISO8601 (iso8601ParseM)+import Kioku.Api.Scope (MemoryScope (..), Namespace (..), ScopeKind (..))+import Kioku.Api.Types (Confidence (..), MemoryType (..))+import Kioku.Id (idText)+import Kioku.Memory.Domain+ ( MemoryArchivedData (..),+ MemoryConfidenceUpdatedData (..),+ MemoryEvent (..),+ MemoryMergedData (..),+ MemoryRecordedData (..),+ MemorySupersededData (..),+ MemoryTagsUpdatedData (..),+ )+import Kioku.Memory.EventStream (parseMemoryEvent)+import Kioku.Session.Domain+ ( InteractiveSessionRecordedData (..),+ SessionAwaitingData (..),+ SessionCompletedData (..),+ SessionEvent (..),+ SessionFailedData (..),+ SessionResumedData (..),+ SessionStartedData (..),+ TurnRecordedData (..),+ )+import Kioku.Session.EventStream (parseSessionEvent)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))++tests :: TestTree+tests =+ testGroup+ "pre-upgrade event payloads still decode"+ [ testGroup "memory events" memoryTests,+ testGroup "session events" sessionTests,+ testGroup "the legacy fallback still exists" fallbackTests+ ]++-- * Memory events++memoryTests :: [TestTree]+memoryTests =+ [ testCase "memory_recorded" do+ decodeMemory memoryRecordedJson >>= \case+ MemoryRecorded d -> do+ idText d.memoryId @?= memoryIdText+ d.agentId @?= "agent-1"+ fmap idText d.sessionId @?= Just sessionIdText+ d.scope @?= fixtureScope+ d.memoryType @?= MemoryFact+ d.content @?= "the build is green"+ d.priority @?= 3+ d.confidence @?= HighConfidence+ d.tags @?= Set.fromList ["build", "ci"]+ d.supersedes @?= Nothing+ d.recordedAt @?= at "2026-06-24T21:30:00Z"+ other -> unexpected "MemoryRecorded" other,+ testCase "memory_superseded" do+ decodeMemory memorySupersededJson >>= \case+ MemorySuperseded d -> do+ idText d.memoryId @?= memoryIdText+ idText d.supersededBy @?= otherMemoryIdText+ d.supersededAt @?= at "2026-06-24T21:31:00Z"+ other -> unexpected "MemorySuperseded" other,+ testCase "memory_archived" do+ decodeMemory memoryArchivedJson >>= \case+ MemoryArchived d -> do+ idText d.memoryId @?= memoryIdText+ d.archivedAt @?= at "2026-06-24T21:32:00Z"+ other -> unexpected "MemoryArchived" other,+ testCase "memory_tags_updated" do+ decodeMemory memoryTagsUpdatedJson >>= \case+ MemoryTagsUpdated d -> do+ idText d.memoryId @?= memoryIdText+ d.tags @?= Set.fromList ["ci", "stable"]+ d.updatedAt @?= at "2026-06-24T21:33:00Z"+ other -> unexpected "MemoryTagsUpdated" other,+ testCase "memory_confidence_updated" do+ decodeMemory memoryConfidenceUpdatedJson >>= \case+ MemoryConfidenceUpdated d -> do+ idText d.memoryId @?= memoryIdText+ d.confidence @?= MediumConfidence+ d.updatedAt @?= at "2026-06-24T21:34:00Z"+ other -> unexpected "MemoryConfidenceUpdated" other,+ testCase "memory_merged" do+ decodeMemory memoryMergedJson >>= \case+ MemoryMerged d -> do+ idText d.memoryId @?= memoryIdText+ idText d.mergedInto @?= otherMemoryIdText+ d.mergedAt @?= at "2026-06-24T21:35:00Z"+ other -> unexpected "MemoryMerged" other+ ]++-- * Session events++sessionTests :: [TestTree]+sessionTests =+ [ testCase "session_started" do+ decodeSession sessionStartedJson >>= \case+ SessionStarted d -> do+ idText d.sessionId @?= sessionIdText+ d.agentId @?= "agent-1"+ d.focus @?= "ship the release"+ d.scope @?= fixtureScope+ d.subjectRef @?= Just "kioku"+ d.previousSessionId @?= Nothing+ d.parentSessionId @?= Nothing+ d.delegationDepth @?= 0+ d.startedAt @?= at "2026-06-24T21:30:00Z"+ other -> unexpected "SessionStarted" other,+ testCase "session_completed" do+ decodeSession sessionCompletedJson >>= \case+ SessionCompleted d -> do+ idText d.sessionId @?= sessionIdText+ d.completedAt @?= at "2026-06-24T21:40:00Z"+ d.modelUsed @?= Just "claude-opus-4-8"+ d.summary @?= Just "planned the day"+ other -> unexpected "SessionCompleted" other,+ testCase "session_failed" do+ decodeSession sessionFailedJson >>= \case+ SessionFailed d -> do+ idText d.sessionId @?= sessionIdText+ d.failedAt @?= at "2026-06-24T21:41:00Z"+ d.errorMessage @?= "model timed out"+ other -> unexpected "SessionFailed" other,+ testCase "session_awaiting" do+ decodeSession sessionAwaitingJson >>= \case+ SessionAwaiting d -> do+ idText d.sessionId @?= sessionIdText+ d.reason @?= "needs human input"+ d.correlationKey @?= Just "approval-1"+ d.deadline @?= Just (at "2026-06-25T21:41:00Z")+ d.awaitedAt @?= at "2026-06-24T21:42:00Z"+ other -> unexpected "SessionAwaiting" other,+ testCase "session_resumed" do+ decodeSession sessionResumedJson >>= \case+ SessionResumed d -> do+ idText d.sessionId @?= sessionIdText+ d.correlationKey @?= Just "approval-1"+ d.force @?= False+ d.input @?= "approved"+ d.resumedAt @?= at "2026-06-24T21:43:00Z"+ other -> unexpected "SessionResumed" other,+ testCase "interactive_session_recorded" do+ decodeSession interactiveSessionRecordedJson >>= \case+ InteractiveSessionRecorded d -> do+ idText d.sessionId @?= sessionIdText+ d.agentId @?= "agent-1"+ d.focus @?= "pairing"+ d.scope @?= fixtureScope+ d.subjectRef @?= Nothing+ d.startedAt @?= at "2026-06-24T21:44:00Z"+ other -> unexpected "InteractiveSessionRecorded" other,+ testCase "turn_recorded" do+ decodeSession turnRecordedJson >>= \case+ TurnRecorded d -> do+ idText d.sessionId @?= sessionIdText+ d.turnId @?= "turn-1"+ d.turnIndex @?= 0+ d.role @?= "user"+ d.content @?= "hello"+ d.toolSummary @?= Just "no tools"+ d.promptTokens @?= Just 12+ d.outputTokens @?= Just 34+ d.recordedAt @?= at "2026-06-24T21:45:00Z"+ other -> unexpected "TurnRecorded" other+ ]++-- * The two-arm fallback++--+-- @parseMemoryEvent@ tries the native parser, then the legacy Rei one, and on a+-- double failure reports both errors. These pin all three behaviours so the+-- fallback cannot be dropped silently.++fallbackTests :: [TestTree]+fallbackTests =+ [ testCase "a legacy Rei payload decodes through the second arm" do+ decodeMemory legacyReiMemoryRecordedJson >>= \case+ MemoryRecorded d -> do+ idText d.memoryId @?= memoryIdText+ d.content @?= "recorded by rei"+ other -> unexpected "MemoryRecorded" other,+ testCase "an undecodable payload reports both arms' errors" do+ case parseMemoryEvent =<< decodeValue "{\"type\":\"not_a_real_event\",\"data\":{}}" of+ Right event -> assertFailure ("expected a decode failure, got " <> show event)+ Left err -> do+ assertContains "legacy decode failed" err+ assertContains "not_a_real_event" err+ ]++-- * Fixtures++--+-- Captured verbatim from @memoryCodec.encode@ / @sessionCodec.encode@ on the+-- pre-upgrade tree (kioku 0.1.0.0, Keiro 0.3). Do not regenerate these from the+-- current code -- that would make the test tautological. Edit only alongside a+-- deliberate, documented wire-format change.++memoryRecordedJson :: ByteString+memoryRecordedJson =+ "{\"data\":{\"agentId\":\"agent-1\",\"confidence\":\"high\",\"content\":\"the build is green\",\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"memoryType\":\"fact\",\"priority\":3,\"recordedAt\":\"2026-06-24T21:30:00Z\",\"scope\":{\"contents\":[\"shikigami\",\"repo\",\"kioku\"],\"tag\":\"ScopeEntity\"},\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"supersedes\":null,\"tags\":[\"build\",\"ci\"]},\"type\":\"memory_recorded\"}"++memorySupersededJson :: ByteString+memorySupersededJson =+ "{\"data\":{\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"supersededAt\":\"2026-06-24T21:31:00Z\",\"supersededBy\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgmf\"},\"type\":\"memory_superseded\"}"++memoryArchivedJson :: ByteString+memoryArchivedJson =+ "{\"data\":{\"archivedAt\":\"2026-06-24T21:32:00Z\",\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\"},\"type\":\"memory_archived\"}"++memoryTagsUpdatedJson :: ByteString+memoryTagsUpdatedJson =+ "{\"data\":{\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"tags\":[\"ci\",\"stable\"],\"updatedAt\":\"2026-06-24T21:33:00Z\"},\"type\":\"memory_tags_updated\"}"++memoryConfidenceUpdatedJson :: ByteString+memoryConfidenceUpdatedJson =+ "{\"data\":{\"confidence\":\"medium\",\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"updatedAt\":\"2026-06-24T21:34:00Z\"},\"type\":\"memory_confidence_updated\"}"++memoryMergedJson :: ByteString+memoryMergedJson =+ "{\"data\":{\"memoryId\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgme\",\"mergedAt\":\"2026-06-24T21:35:00Z\",\"mergedInto\":\"kioku_memory_01kvxa7d2cezhs874g3n8dfgmf\"},\"type\":\"memory_merged\"}"++sessionStartedJson :: ByteString+sessionStartedJson =+ "{\"data\":{\"agentId\":\"agent-1\",\"delegationDepth\":0,\"focus\":\"ship the release\",\"parentSessionId\":null,\"previousSessionId\":null,\"scope\":{\"contents\":[\"shikigami\",\"repo\",\"kioku\"],\"tag\":\"ScopeEntity\"},\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"startedAt\":\"2026-06-24T21:30:00Z\",\"subjectRef\":\"kioku\"},\"type\":\"session_started\"}"++sessionCompletedJson :: ByteString+sessionCompletedJson =+ "{\"data\":{\"completedAt\":\"2026-06-24T21:40:00Z\",\"modelUsed\":\"claude-opus-4-8\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"summary\":\"planned the day\"},\"type\":\"session_completed\"}"++sessionFailedJson :: ByteString+sessionFailedJson =+ "{\"data\":{\"errorMessage\":\"model timed out\",\"failedAt\":\"2026-06-24T21:41:00Z\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\"},\"type\":\"session_failed\"}"++sessionAwaitingJson :: ByteString+sessionAwaitingJson =+ "{\"data\":{\"awaitedAt\":\"2026-06-24T21:42:00Z\",\"correlationKey\":\"approval-1\",\"deadline\":\"2026-06-25T21:41:00Z\",\"reason\":\"needs human input\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\"},\"type\":\"session_awaiting\"}"++sessionResumedJson :: ByteString+sessionResumedJson =+ "{\"data\":{\"correlationKey\":\"approval-1\",\"force\":false,\"input\":\"approved\",\"resumedAt\":\"2026-06-24T21:43:00Z\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\"},\"type\":\"session_resumed\"}"++interactiveSessionRecordedJson :: ByteString+interactiveSessionRecordedJson =+ "{\"data\":{\"agentId\":\"agent-1\",\"focus\":\"pairing\",\"scope\":{\"contents\":[\"shikigami\",\"repo\",\"kioku\"],\"tag\":\"ScopeEntity\"},\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"startedAt\":\"2026-06-24T21:44:00Z\",\"subjectRef\":null},\"type\":\"interactive_session_recorded\"}"++turnRecordedJson :: ByteString+turnRecordedJson =+ "{\"data\":{\"content\":\"hello\",\"outputTokens\":34,\"promptTokens\":12,\"recordedAt\":\"2026-06-24T21:45:00Z\",\"role\":\"user\",\"sessionId\":\"kioku_session_01kvxa7d2cezhs874g3n8dfgme\",\"toolSummary\":\"no tools\",\"turnId\":\"turn-1\",\"turnIndex\":0},\"type\":\"turn_recorded\"}"++-- | A Rei-format payload, which only the legacy arm of 'parseMemoryEvent' understands.+legacyReiMemoryRecordedJson :: ByteString+legacyReiMemoryRecordedJson =+ "{\"type\":\"agent_memory_recorded\",\"data\":{\"memoryId\":\"agent_memory_01kvxa7d2cezhs874g3n8dfgme\",\"agentId\":\"agent-1\",\"anchor\":{\"type\":\"intention\",\"id\":\"intention_demo\"},\"memoryType\":\"fact\",\"content\":\"recorded by rei\",\"confidence\":\"high\",\"tags\":[\"build\"],\"recordedAt\":\"2026-06-24T21:30:00Z\"}}"++-- * Helpers++fixtureScope :: MemoryScope+fixtureScope = ScopeEntity (Namespace "shikigami") (ScopeKind "repo") "kioku"++memoryIdText :: Text+memoryIdText = "kioku_memory_01kvxa7d2cezhs874g3n8dfgme"++otherMemoryIdText :: Text+otherMemoryIdText = "kioku_memory_01kvxa7d2cezhs874g3n8dfgmf"++sessionIdText :: Text+sessionIdText = "kioku_session_01kvxa7d2cezhs874g3n8dfgme"++at :: String -> UTCTime+at = maybe (error "CodecCompatSpec: malformed fixture timestamp") id . iso8601ParseM++decodeValue :: ByteString -> Either Text Value+decodeValue = first Text.pack . eitherDecode++decodeMemory :: ByteString -> IO MemoryEvent+decodeMemory = either (assertFailure . Text.unpack) pure . (parseMemoryEvent <=< decodeValue)++decodeSession :: ByteString -> IO SessionEvent+decodeSession = either (assertFailure . Text.unpack) pure . (parseSessionEvent <=< decodeValue)++unexpected :: (Show event) => String -> event -> IO ()+unexpected expected got = assertFailure ("Expected " <> expected <> ", got " <> show got)++assertContains :: Text -> Text -> IO ()+assertContains needle haystack =+ if needle `Text.isInfixOf` haystack+ then pure ()+ else assertFailure ("expected " <> show needle <> " in " <> show haystack)
test/Main.hs view
@@ -1,6 +1,7 @@ module Main where import Kioku.AwaitingSpec qualified as AwaitingSpec+import Kioku.CodecCompatSpec qualified as CodecCompatSpec import Kioku.DistillSpec qualified as DistillSpec import Kioku.EmbeddingWorkerSpec qualified as EmbeddingWorkerSpec import Kioku.IdempotencySpec qualified as IdempotencySpec@@ -21,6 +22,7 @@ testGroup "kioku" [ AwaitingSpec.tests,+ CodecCompatSpec.tests, ReiCompatSpec.tests, IdempotencySpec.tests, ReadModelReconcileSpec.tests,