packages feed

baikai 0.10.1.0 → 0.10.1.1

raw patch · 4 files changed

+84/−14 lines, 4 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -7,6 +7,22 @@  ## [Unreleased] +## [baikai-openai 0.7.3.1] - 2026-11-14++### Fixed++- Require `baikai ^>=0.10.1.1` in library and tests so new adapter consumers+  receive the Codex final-message correction.++## [baikai 0.10.1.1] - 2026-11-14++### Fixed++- `baikai`: the Codex batch parser returns the last completed agent message+  instead of concatenating complete messages, preserving structured JSON after+  intermediate commentary. Started and updated items cannot replace the final+  answer; usage and thread observations still cover the whole run ([BUG-2](docs/bug-reports/codex-concatenates-complete-agent-messages.md)).+ ## [baikai 0.10.1.0] - 2026-11-03  ### Added
baikai.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: baikai-version: 0.10.1.0+version: 0.10.1.1 synopsis: Unified Haskell interface for multiple AI providers description:   baikai provides a unified, provider-agnostic Haskell interface for working
src/Baikai/Provider/Cli/Internal.hs view
@@ -205,7 +205,7 @@ -- must be recorded as 'Baikai.Evidence.Unobserved' downstream and must -- never be filled in from the request. data CodexRunReport = CodexRunReport-  { -- | The concatenated text of every @agent_message@ event.+  { -- | The last completed @agent_message@ body, or empty text if none.     message :: !Text,     -- | Codex's own handle for the conversation, from the     -- thread-start event.@@ -221,10 +221,10 @@  -- | The accumulator 'parseCodexJsonlStream' folds events into. ----- Separate from 'CodexRunReport' only because the message text arrives--- in pieces and is kept reversed until the fold finishes.+-- Complete message bodies replace the candidate, including an empty body.+-- They are independent answers rather than fragments to concatenate. data CodexAccumulator = CodexAccumulator-  { messages :: ![Text],+  { lastMessage :: !(Maybe Text),     threadId :: !(Maybe Text),     reportedModel :: !(Maybe Text),     usage :: !(Maybe Usage)@@ -234,7 +234,7 @@ emptyCodexAccumulator :: CodexAccumulator emptyCodexAccumulator =   CodexAccumulator-    { messages = [],+    { lastMessage = Nothing,       threadId = Nothing,       reportedModel = Nothing,       usage = Nothing@@ -243,6 +243,8 @@ -- | Consume a stream of stdout bytes from @codex exec --json@, split on -- newlines, decode each line as JSON, and fold the events into what the -- run reported about itself.+-- Only the last completed agent message supplies the answer; observations+-- are collected independently from the entire stream. -- -- A line that is not valid JSON is skipped rather than failing the run: -- codex writes progress chatter to stderr, but a future version writing@@ -269,7 +271,7 @@   let acc = absorbLine folded (joinPieces pending)   pure     CodexRunReport-      { message = Text.concat (reverse (acc ^. #messages)),+      { message = fromMaybe "" (acc ^. #lastMessage),         threadId = acc ^. #threadId,         reportedModel = acc ^. #reportedModel,         usage = acc ^. #usage@@ -296,7 +298,7 @@ absorbCodexEvent acc v =   withTurn     ( acc-        & #messages %~ maybe id (:) (extractAgentMessage v)+        & #lastMessage %~ (extractAgentMessage v <|>)         & #threadId %~ (<|> extractThreadId v)     )   where@@ -308,8 +310,8 @@           & #reportedModel .~ reported  -- | Best-effort extractor for the assistant text inside a single--- Codex @--json@ event. See the original implementation's--- documentation for the schema variants accepted.+-- Codex @--json@ event. Modern @item@ envelopes must be @item.completed@;+-- older flat and @msg@-nested agent messages are also complete bodies. extractAgentMessage :: Value -> Maybe Text extractAgentMessage = parseMaybe parser   where@@ -317,10 +319,11 @@       mItem <- o .:? "item"       case (mItem :: Maybe Value) of         Just (Aeson.Object io) -> do+          eventType <- o .:? "type"           ty <- io .:? "type"-          case (ty :: Maybe Text) of-            Just "agent_message" -> pickText io-            _ -> tryMsg o+          case (eventType :: Maybe Text, ty :: Maybe Text) of+            (Just "item.completed", Just "agent_message") -> pickText io+            _ -> fail "not a completed agent_message"         _ -> tryMsg o      tryMsg o = do
test/CliInternalSpec.hs view
@@ -16,6 +16,7 @@ import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.ByteString.Char8 qualified as BS8+import Data.ByteString.Lazy qualified as LBS import Data.Generics.Labels () import Data.List (isInfixOf) import Data.Text qualified as Text@@ -80,7 +81,53 @@ codexParserTests =   testGroup     "codex exec --json event stream"-    [ testCase "a recorded run yields its text, thread id, and token counts" $ do+    [ testCase "two complete JSON answers return only the final object" $ do+        let answer why = Aeson.object ["why" Aeson..= (why :: Text.Text), "what" Aeson..= ("X" :: Text.Text), "blocking" Aeson..= ("B" :: Text.Text), "status" Aeson..= ("S" :: Text.Text)]+            encoded = Text.decodeUtf8 . LBS.toStrict . Aeson.encode+        report <- parseCodex [codexMessage "item.completed" (encoded (answer "Intermediate")), codexMessage "item.completed" (encoded (answer "Final"))]+        Aeson.eitherDecodeStrict' (Text.encodeUtf8 (report ^. #message)) @?= Right (answer "Final"),+      testCase "three complete messages replace rather than append" $ do+        report <- parseCodex (map (codexMessage "item.completed") ["first", "second", "third"])+        report ^. #message @?= "third",+      testCase "prose commentary is excluded from a final JSON answer" $ do+        report <- parseCodex (map (codexMessage "item.completed") ["Working on it.", "{\"why\":\"Final\"}"])+        report ^. #message @?= "{\"why\":\"Final\"}",+      testCase "an empty completed message replaces preceding text" $ do+        report <- parseCodex (map (codexMessage "item.completed") ["commentary", ""])+        report ^. #message @?= "",+      testCase "malformed final answer text replaces earlier valid JSON" $ do+        report <- parseCodex (map (codexMessage "item.completed") ["{\"why\":\"valid\"}", "{\"why\":"])+        report ^. #message @?= "{\"why\":"+        assertBool "the selected body is still invalid JSON" (case Aeson.eitherDecodeStrict' @Aeson.Value (Text.encodeUtf8 (report ^. #message)) of Left _ -> True; Right _ -> False),+      testCase "started and updated items never replace a completed answer" $ do+        report <-+          parseCodex+            [ codexMessage "item.started" "before",+              codexMessage "item.completed" "first",+              codexMessage "item.updated" "between",+              codexMessage "item.completed" "final",+              codexMessage "item.started" "after-start",+              codexMessage "item.updated" "after-update",+              "{\"type\":\"item.completed\",\"item\":{\"type\":\"reasoning\",\"text\":\"reasoning\"}}\n",+              "{\"type\":\"item.completed\",\"item\":{\"type\":\"command_execution\",\"text\":\"tool\"}}\n"+            ]+        report ^. #message @?= "final",+      testCase "only incomplete items yield the existing no-answer result" $ do+        report <- parseCodex [codexMessage "item.started" "pending", codexMessage "item.updated" "partial"]+        report ^. #message @?= "",+      testCase "legacy complete messages also replace their predecessors" $ do+        nested <- parseCodex ["{\"msg\":{\"type\":\"agent_message\",\"message\":\"first\"}}\n{\"msg\":{\"type\":\"agent_message\",\"message\":\"nested-final\"}}\n"]+        nested ^. #message @?= "nested-final"+        flat <- parseCodex ["{\"type\":\"agent_message\",\"message\":\"first\"}\n{\"type\":\"agent_message\",\"text\":\"flat-final\"}\n"]+        flat ^. #message @?= "flat-final",+      testCase "message replacement survives byte chunks and a final missing newline" $ do+        let bytes = codexMessage "item.completed" "earlier" <> BS.init (codexMessage "item.completed" "final λ")+        report <- parseCodex (map BS.singleton (BS.unpack bytes))+        report ^. #message @?= "final λ",+      testCase "malformed outer lines cannot displace the last completed answer" $ do+        report <- parseCodex [codexMessage "item.completed" "answer", "{broken\n", "{\"type\":\"item.completed\",\"item\":{\"type\":\"agent_message\"}}\n", "{\"type\":\"future-event\"}\n"]+        report ^. #message @?= "answer",+      testCase "a recorded run yields its text, thread id, and token counts" $ do         recorded <- BS.readFile "test/fixtures/codex-events.jsonl"         report <- parseCodex [recorded]         report ^. #message @?= "ok"@@ -189,6 +236,10 @@               "assembling one two-megabyte event did not finish within ten seconds"           Just report -> Text.length (report ^. #message) @?= 2000000     ]++codexMessage :: Text.Text -> Text.Text -> ByteString+codexMessage eventType body =+  LBS.toStrict (Aeson.encode (Aeson.object ["type" Aeson..= eventType, "item" Aeson..= Aeson.object ["type" Aeson..= ("agent_message" :: Text.Text), "text" Aeson..= body]])) <> "\n"  -- ============================================================ -- The claude result document