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 +12/−0
- shikumi-compile.cabal +5/−5
- src/Shikumi/Compile/ChainOfThought.hs +8/−1
- src/Shikumi/Compile/RAG.hs +19/−14
- src/Shikumi/Compile/Serialize.hs +66/−26
- test/Main.hs +38/−3
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 ]