shikumi-tools-0.3.0.0: test/ProtocolSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | M3: the native-vs-prompt protocol seam. The same @react@ program reaches the
-- same typed answer under either forced protocol, and 'ProtocolAuto' resolves
-- conservatively (a CLI model — whose tools baikai silently drops — picks the
-- prompt path; a native-capable model picks the native path).
module ProtocolSpec (tests) where
import Baikai (Api (..), _Model)
import Control.Lens ((&), (.~))
import Data.Aeson (object, (.=))
import Data.Generics.Labels ()
import Data.Text (Text)
import Data.Vector qualified as V
import Fixtures
( WeatherResp,
expectedWeather,
nativeScript,
promptScript,
weatherQuestion,
weatherRegistry,
weatherSignature,
)
import MockLLM (mkTextResponse, mkToolCallsResponse, runAgent)
import Shikumi.Agent.ReAct
( Action (..),
Step (..),
Termination (..),
ToolProtocol (..),
Trajectory (..),
defaultReActConfig,
reactWithTrajectory,
resolveProtocolKind,
)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
tests :: TestTree
tests =
testGroup
"Protocol"
[ testCase "native and prompt paths agree on the typed answer" $ do
nat <-
runAgent
nativeScript
(reactWithTrajectory weatherSignature weatherRegistry (defaultReActConfig & #protocol .~ ProtocolNative))
weatherQuestion
pro <-
runAgent
promptScript
(reactWithTrajectory weatherSignature weatherRegistry (defaultReActConfig & #protocol .~ ProtocolPrompt))
weatherQuestion
case (nat, pro) of
(Right (oN :: WeatherResp, tN), Right (oP, tP)) -> do
oN @?= expectedWeather
oP @?= expectedWeather
termination tN @?= TerminatedFinish
termination tP @?= TerminatedFinish
V.length (steps tN) @?= 2
V.length (steps tP) @?= 2
_ -> assertFailure "both protocols should succeed",
testCase "ProtocolAuto picks prompt for a CLI model" $
resolveProtocolKind ProtocolAuto (_Model & #api .~ AnthropicMessagesCli) @?= ProtocolPrompt,
testCase "ProtocolAuto picks native for a native-capable model" $
resolveProtocolKind ProtocolAuto (_Model & #provider .~ "openai" & #api .~ OpenAIChatCompletions) @?= ProtocolNative,
testCase "native turn with two tool calls executes both in order" $ do
let parisArgs = object ["city" .= ("Paris" :: Text), "units" .= ("c" :: Text)]
londonArgs = object ["city" .= ("London" :: Text), "units" .= ("c" :: Text)]
script =
[ mkToolCallsResponse [("c1", "get_weather", parisArgs), ("c2", "get_weather", londonArgs)],
mkTextResponse "done",
mkTextResponse "{\"tempC\": 12.0, \"summary\": \"mild\"}"
]
out <-
runAgent
script
(reactWithTrajectory weatherSignature weatherRegistry (defaultReActConfig & #protocol .~ ProtocolNative))
weatherQuestion
case out of
Right (answer :: WeatherResp, traj) -> do
answer @?= expectedWeather
termination traj @?= TerminatedFinish
case V.toList (steps traj) of
[s1, s2, s3] -> do
action s1 @?= CallTool "get_weather" parisArgs
action s2 @?= CallTool "get_weather" londonArgs
action s3 @?= Finish
other -> assertFailure ("expected two tool-call steps followed by finish, got " <> show other)
Left err -> assertFailure ("native multi-call run failed: " <> show err)
]