packages feed

shikumi-compile 0.1.1.0 → 0.2.0.0

raw patch · 6 files changed

+148/−49 lines, 6 filesdep ~baikaidep ~shikumidep ~shikumi-compilePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: baikai, shikumi, shikumi-compile

API changes (from Hackage documentation)

+ Shikumi.Compile.Serialize: instance Data.Aeson.Types.FromJSON.FromJSON Shikumi.Compile.Serialize.CompiledState
+ Shikumi.Compile.Serialize: instance Data.Aeson.Types.ToJSON.ToJSON Shikumi.Compile.Serialize.CompiledState
+ Shikumi.Compile.Serialize: instance GHC.Internal.Generics.Generic Shikumi.Compile.Serialize.CompiledState

Files

CHANGELOG.md view
@@ -2,6 +2,18 @@  ## Unreleased +## 0.2.0.0 - 2026-07-05++### Changed++- **BREAKING** Compiled state JSON now stores a structural shape fingerprint plus+  parameters. Legacy bare parameter arrays are rejected and must be re-encoded.+- `decodeCompiledOnto` now fails with a shape-mismatch error when saved state is+  loaded onto the wrong compiled program template.+- RAG now stores retrieved context in serializable node parameters, so RAG state+  survives compiled-state encode/decode.+- Refreshed the internal `shikumi` bound for the `0.3` series.+ ## 0.1.1.0 - 2026-06-28  ### Changed
shikumi-compile.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.4 name:            shikumi-compile-version:         0.1.1.0+version:         0.2.0.0 synopsis:        The compiler layer for shikumi LM programs (EP-9) category:        AI description:@@ -54,7 +54,7 @@     , effectful     , generic-lens  ^>=2.2     , lens          ^>=5.3-    , shikumi       ^>=0.2.0.0+    , shikumi       ^>=0.3.0.0     , text          ^>=2.1  test-suite shikumi-compile-test@@ -69,14 +69,14 @@    build-depends:     , aeson-    , baikai           >=0.2      && <0.3+    , baikai           >=0.3      && <0.4     , base     , bytestring     , effectful     , generic-lens     , lens-    , shikumi          ^>=0.2.0.0-    , shikumi-compile  ^>=0.1.1.0+    , shikumi          ^>=0.3.0.0+    , shikumi-compile  ^>=0.2.0.0     , tasty     , tasty-hunit     , text
src/Shikumi/Compile/ChainOfThought.hs view
@@ -31,6 +31,11 @@ -- carries demos, they must be chain-of-thought-shaped (include a @reasoning@ field) -- to decode under the augmented output; otherwise compile chain-of-thought before -- few-shot.+--+-- Serialized state from this compiler must be decoded onto the same compiled+-- shape, not onto the plain base program. In practice, re-apply+-- 'chainOfThoughtCompiler' to the base template before calling+-- 'Shikumi.Compile.Serialize.decodeCompiledOnto'. module Shikumi.Compile.ChainOfThought   ( chainOfThoughtCompiler,   )@@ -57,6 +62,8 @@  -- | Rewrite every 'Predict' node into its chain-of-thought form, recursing through -- every composite/combinator node so nodes nested arbitrarily deep are reached.+-- Saved state produced by this compiler should be loaded onto this compiled shape,+-- not onto the original base program. chainOfThoughtCompiler :: Compiler chainOfThoughtCompiler = Compiler cot @@ -73,6 +80,6 @@ cot (Retry n p) = Retry n (cot p) cot (RetryWhen ok n p) = RetryWhen ok n (cot p) cot (Validate v p) = Validate v (cot p)-cot (MajorityVote k sched p) = MajorityVote k sched (cot p)+cot (MajorityVote k sched r p) = MajorityVote k sched r (cot p) cot (Ensemble ps reduce) = Ensemble (map cot ps) reduce cot (Embed f) = Embed f -- an agent node has no 'Predict' to rewrite; pass through
src/Shikumi/Compile/RAG.hs view
@@ -16,27 +16,30 @@ -- query-independent of the actual program input; wiring true per-input retrieval -- awaits an EP-4 embed node and is left as a TODO. ----- The injection is a /structural/ rewrite (like "Shikumi.Compile.ChainOfThought"):--- each @Predict sig ps@ leaf becomes @Predict (setInstruction (base <> context)--- sig) ps@, so the original task instruction is preserved /and/ the retrieved--- context is added (it appears in the rendered system prompt). The node's existing--- 'Shikumi.Program.Params' are untouched; the same @instructionOverride@-precedence--- caveat as chain-of-thought applies (an override set on a node replaces the whole--- instruction at run time, context included), so apply 'rag' before a zero-shot--- instruction if both are wanted.+-- The injection is a serializable parameter rewrite: each @Predict sig ps@ leaf+-- keeps its signature and receives an @instructionOverride@ containing the+-- effective instruction (an existing override if present, otherwise the+-- signature's base instruction) plus the retrieved context. This means RAG state+-- survives 'Shikumi.Compile.Serialize.encodeCompiled' /+-- 'Shikumi.Compile.Serialize.decodeCompiledOnto'. Composition order still matters:+-- applying 'zeroShot' after 'rag' replaces the whole override and drops the+-- context, while applying 'rag' after 'zeroShot' appends context to the zero-shot+-- instruction. module Shikumi.Compile.RAG   ( rag,     formatPassages,   ) where +import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text qualified as T import Effectful (runPureEff) import Shikumi.Compile.Retriever (Passage (..), Retriever (..)) import Shikumi.Compile.Types (Compiler (..)) import Shikumi.Program-  ( Program+  ( Params (..),+    Program       ( Compose,         Embed,         Ensemble,@@ -50,7 +53,7 @@         Validate       ),   )-import Shikumi.Signature (getInstruction, setInstruction)+import Shikumi.Signature (getInstruction)  -- | Install retrieved context at every node. Retrieval happens once, now, against -- @query@ (the documented compile-time fallback); the formatted passages are then@@ -69,15 +72,17 @@   "Use the following retrieved context to answer:\n"     <> T.unlines ["- " <> text p | p <- ps] --- | Append the context block to every node's signature instruction, recursing--- through every composite/combinator node. A no-op when @ctx@ is empty.+-- | Append the context block to every node's effective instruction, storing the+-- result in serializable node parameters. A no-op when @ctx@ is empty. install :: Text -> Program i o -> Program i o install ctx   | T.null ctx = id   | otherwise = go   where     go :: forall i o. Program i o -> Program i o-    go (Predict sig ps) = Predict (setInstruction (getInstruction sig <> "\n\n" <> ctx) sig) ps+    go (Predict sig ps) =+      let base = fromMaybe (getInstruction sig) (instructionOverride ps)+       in Predict sig ps {instructionOverride = Just (base <> "\n\n" <> ctx)}     go (Compose a b) = Compose (go a) (go b)     go (FMap k p) = FMap k (go p)     go (Map w p) = Map w (go p)@@ -85,6 +90,6 @@     go (Retry n p) = Retry n (go p)     go (RetryWhen ok n p) = RetryWhen ok n (go p)     go (Validate v p) = Validate v (go p)-    go (MajorityVote k sched p) = MajorityVote k sched (go p)+    go (MajorityVote k sched r p) = MajorityVote k sched r (go p)     go (Ensemble ps reduce) = Ensemble (map go ps) reduce     go (Embed f) = Embed f -- an agent node has no 'Predict' to rewrite; pass through
src/Shikumi/Compile/Serialize.hs view
@@ -1,57 +1,97 @@ -- | Serialization of a 'CompiledProgram'. ----- __Parameter-state, not whole-structure.__ A @Program@'s structure cannot be--- serialized in general (its @FMap@ nodes hold opaque functions and its GADT type--- indices are not reflected at runtime). What /can/ be saved — and is exactly what--- the optimizer (EP-10) and CLI (EP-12) need — is each node's tunable--- 'Shikumi.Program.Params', against a known program /template/. This mirrors DSPy's+-- __Parameter-state plus shape fingerprint, not whole-structure.__ A @Program@'s+-- executable structure cannot be serialized in general (its @FMap@ nodes hold+-- opaque functions and its GADT type indices are not reflected at runtime). What+-- /can/ be saved — and is exactly what the optimizer (EP-10) and CLI (EP-12)+-- need — is each node's tunable 'Shikumi.Program.Params', plus a closure-free+-- structural fingerprint used to reject the wrong /template/. This mirrors DSPy's -- @dump_state@/@load_state@: dump the parameters, load them back onto a program you -- still hold as code. -- -- This reuses EP-4's already-provided parameter interface verbatim:--- 'Shikumi.Program.programParams' (the ordered @[Params]@ vector, in @foldParams@--- order) and 'Shikumi.Program.setProgramParams' (re-apply a vector onto a matching--- program, with a length-mismatch guard). So this module is a thin JSON wrapper —--- the node-order contract and the count check are EP-4's, not re-invented here.+-- 'Shikumi.Program.programShape', 'Shikumi.Program.programParams' (the ordered+-- @[Params]@ vector, in @foldParams@ order), and+-- 'Shikumi.Program.setProgramParams' (re-apply a vector onto a matching program,+-- with a length-mismatch guard). So this module is a thin JSON wrapper — the shape+-- and node-order contracts are EP-4's, not re-invented here. module Shikumi.Compile.Serialize   ( encodeCompiled,     decodeCompiledOnto,   ) where -import Data.Aeson (eitherDecode, encode)+import Data.Aeson (FromJSON, ToJSON, eitherDecode, encode) import Data.ByteString.Lazy (ByteString)+import GHC.Generics (Generic) import Shikumi.Compile.Types (CompiledProgram (..)) import Shikumi.Program   ( Params,     Program,+    ProgramShape,     ProgramShapeError (..),     programParams,+    programShape,     setProgramParams,   ) --- | Emit a compiled program's ordered parameter vector as JSON (a JSON array of--- @Params@, in 'Shikumi.Program.foldParams' order).+-- | The persisted form of a compiled program: a closure-free structural+-- fingerprint plus the ordered parameter vector.+data CompiledState = CompiledState+  { shape :: !ProgramShape,+    params :: ![Params]+  }+  deriving stock (Generic)++instance ToJSON CompiledState++instance FromJSON CompiledState++-- | Emit a compiled program's shape fingerprint and ordered parameter vector as+-- JSON. encodeCompiled :: CompiledProgram i o -> ByteString-encodeCompiled (CompiledProgram p) = encode (programParams p)+encodeCompiled (CompiledProgram p) = encode (CompiledState (programShape p) (programParams p)) --- | Re-apply a saved parameter vector onto a structural /template/ (the base--- program, held as code), in node order. Fails with a descriptive message if the--- JSON is malformed or if its node count does not match the template — which would--- mean the template is not the program the parameters were saved from. The signature--- mirrors DSPy's @load_state(program)@.+-- | Re-apply saved state onto a structural /template/ (the program, held as code),+-- in node order. Fails with a descriptive message if the JSON is malformed, if the+-- shape fingerprint does not match the template, or if the parameter count is+-- inconsistent with the template. The template must have the same compiled shape+-- that produced the saved state: for structural compilers such as+-- 'Shikumi.Compile.ChainOfThought.chainOfThoughtCompiler', re-apply the same+-- compiler to the base program before loading. The signature mirrors DSPy's+-- @load_state(program)@. decodeCompiledOnto ::   Program i o ->   ByteString ->   Either String (CompiledProgram i o) decodeCompiledOnto template bs = do-  ps <- eitherDecode bs :: Either String [Params]-  case setProgramParams ps template of-    Left (ParamCountMismatch (expected, got)) ->+  st <-+    either+      (Left . decodeError)+      Right+      (eitherDecode bs :: Either String CompiledState)+  let templateShape = programShape template+  if shape st /= templateShape+    then       Left-        ( "shikumi-compile: parameter count mismatch — the template has "-            <> show expected-            <> " node(s) but the saved state has "-            <> show got+        ( "shikumi-compile: shape mismatch — the saved state was produced by a structurally different program. saved shape: "+            <> show (shape st)+            <> "; template shape: "+            <> show templateShape         )-    Right prog -> Right (CompiledProgram prog)+    else applyParams (params st)+  where+    decodeError e =+      "shikumi-compile: expected a compiled-state envelope with shape and params; "+        <> "legacy bare parameter arrays are not supported and must be re-encoded. Aeson error: "+        <> e+    applyParams ps =+      case setProgramParams ps template of+        Left (ParamCountMismatch (expected, got)) ->+          Left+            ( "shikumi-compile: parameter count mismatch — the template has "+                <> show expected+                <> " node(s) but the saved state has "+                <> show got+            )+        Right prog -> Right (CompiledProgram prog)
test/Main.hs view
@@ -3,6 +3,7 @@ -- the nodes/ (a pure @foldParams@ read) — never merely "a type was added". module Main (main) where +import Data.Aeson qualified as Aeson import Data.Text (Text) import Data.Text qualified as T import Effectful (runPureEff)@@ -210,10 +211,44 @@             (origPrompts, _) <- runWithCapture [answerResponse] (compiledProgram compiled) q             (rtPrompts, _) <- runWithCapture [answerResponse] (compiledProgram roundTripped) q             rtPrompts @?= origPrompts,-      testCase "decoding onto a wrong-shaped template returns Left" $ do+      testCase "decoding onto a wrong-shaped template returns a shape Left" $ do         -- a 2-node payload onto a 1-node template         let bytes = encodeCompiled (compile (zeroShot "x") qaPipeline)         case decodeCompiledOnto qaBase bytes of-          Left _ -> pure ()-          Right _ -> assertBool "expected a count-mismatch Left" False+          Left e -> assertBool "shape mismatch mentioned" (contains "shape mismatch" (T.pack e))+          Right _ -> assertBool "expected a shape-mismatch Left" False,+      testCase "legacy bare Params arrays are rejected with a re-encode hint" $ do+        let bytes = Aeson.encode [Params (Just "legacy instruction") []]+        case decodeCompiledOnto qaBase bytes of+          Left e -> do+            assertBool "legacy format mentioned" (contains "legacy bare parameter arrays" (T.pack e))+            assertBool "re-encode hint mentioned" (contains "re-encoded" (T.pack e))+          Right _ -> assertBool "expected a legacy-format Left" False,+      testCase "RAG context survives encode/decode" $ do+        let compiled = compile (rag (inMemoryRetriever 1 corpus) "shikumi mechanism") qaBase+            bytes = encodeCompiled compiled+        case decodeCompiledOnto qaBase bytes of+          Left e -> assertBool ("unexpected decode failure: " <> e) False+          Right roundTripped -> do+            (origPrompts, _) <- runWithCapture [answerResponse] (compiledProgram compiled) q+            (rtPrompts, _) <- runWithCapture [answerResponse] (compiledProgram roundTripped) q+            rtPrompts @?= origPrompts+            case rtPrompts of+              [p] -> assertBool "target passage present after round-trip" (contains "mechanism behind how a system works" p)+              _ -> assertBool "one prompt" False,+      testCase "CoT state does not decode onto the base template" $ do+        let compiled = compile chainOfThoughtCompiler qaBase+            bytes = encodeCompiled compiled+        case decodeCompiledOnto qaBase bytes of+          Left e -> assertBool "shape mismatch mentioned" (contains "shape mismatch" (T.pack e))+          Right _ -> assertBool "expected a shape-mismatch Left" False,+      testCase "CoT round-trips onto the CoT template" $ do+        let compiled = compile chainOfThoughtCompiler qaBase+            bytes = encodeCompiled compiled+        case decodeCompiledOnto (compiledProgram compiled) bytes of+          Left e -> assertBool ("unexpected decode failure: " <> e) False+          Right roundTripped -> do+            (origPrompts, _) <- runWithCapture [cotAnswerResponse] (compiledProgram compiled) q+            (rtPrompts, _) <- runWithCapture [cotAnswerResponse] (compiledProgram roundTripped) q+            rtPrompts @?= origPrompts     ]