@@ -3,7 +3,8 @@ import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
33import { afterAll , beforeAll , beforeEach , describe , expect , test } from "bun:test"
44import { SessionV1 } from "@opencode-ai/core/v1/session"
55import path from "path"
6- import { tool , type ModelMessage } from "ai"
6+ import { InvalidToolInputError , tool , type ModelMessage } from "ai"
7+ import { JSONParseError } from "@ai-sdk/provider"
78import { Cause , Effect , Exit , Fiber , Layer , Stream } from "effect"
89import { InstanceRef } from "../../src/effect/instance-ref"
910import { HttpClientRequest , HttpClientResponse } from "effect/unstable/http"
@@ -27,6 +28,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
2728import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
2829import { LayerNode } from "@opencode-ai/core/effect/layer-node"
2930import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
31+ import { Truncate } from "@/tool/truncate"
3032
3133type ConfigModel = NonNullable < NonNullable < ConfigV1 . Info [ "provider" ] > [ string ] [ "models" ] > [ string ]
3234
@@ -172,6 +174,67 @@ describe("session.llm.hasToolCalls", () => {
172174 } )
173175} )
174176
177+ describe ( "session.llm.invalidToolCallInput" , ( ) => {
178+ test ( "bounds JSON parsing errors stored in repaired tool calls" , ( ) => {
179+ const dropped = "GIANT_MIDDLE_SHOULD_NOT_BE_STORED"
180+ const malformed = `{"query":"USEFUL_PREFIX:${ "x" . repeat ( 100_000 ) } ${ dropped } ${ "y" . repeat ( 100_000 ) } :USEFUL_INPUT_TAIL`
181+ expect ( malformed . length ) . toBeGreaterThan ( 200_000 )
182+
183+ let cause : unknown
184+ try {
185+ JSON . parse ( malformed )
186+ } catch ( error ) {
187+ cause = error
188+ }
189+ if ( ! cause ) throw new Error ( "expected malformed input to fail JSON parsing" )
190+
191+ const failed = new InvalidToolInputError ( {
192+ toolName : "lookup" ,
193+ toolInput : malformed ,
194+ cause : new JSONParseError ( { text : malformed , cause } ) ,
195+ } )
196+ expect ( failed . message . length ) . toBeGreaterThan ( 200_000 )
197+
198+ const storedSchema = z . object ( { tool : z . string ( ) , error : z . string ( ) } )
199+ const stored = LLM . invalidToolCallInput ( "lookup" , failed . message )
200+ const repaired = storedSchema . parse ( JSON . parse ( stored ) )
201+ const omission = repaired . error . match ( / \n \. \. \. ( \d + ) c h a r a c t e r s o m i t t e d \. \. \. \n / )
202+
203+ expect ( stored . length ) . toBeLessThanOrEqual ( Truncate . MAX_ERROR_CHARS )
204+ expect ( repaired . error . length ) . toBeLessThanOrEqual ( Truncate . MAX_ERROR_CHARS )
205+ expect ( repaired . tool ) . toBe ( "lookup" )
206+ expect ( repaired . error ) . toContain ( "USEFUL_PREFIX" )
207+ expect ( repaired . error ) . toContain ( "USEFUL_INPUT_TAIL" )
208+ expect ( repaired . error ) . toContain ( "JSON Parse error: Unterminated string" )
209+ expect ( omission ) . not . toBeNull ( )
210+ if ( ! omission ) throw new Error ( "expected repaired error to report omitted characters" )
211+ expect ( Number ( omission [ 1 ] ) ) . toBe ( failed . message . length - ( repaired . error . length - omission [ 0 ] . length ) )
212+ expect ( repaired . error ) . not . toContain ( dropped )
213+
214+ const short = "short tool error with exact whitespace\n"
215+ const unchanged = storedSchema . parse ( JSON . parse ( LLM . invalidToolCallInput ( "lookup" , short ) ) )
216+ expect ( unchanged . error ) . toBe ( short )
217+
218+ const whitespaceMalformed =
219+ '{"description":"Test resumed player extraction","code":"const x=1;' + "\n\t" . repeat ( 118_000 ) + '"}'
220+ let whitespaceCause : unknown
221+ try {
222+ JSON . parse ( whitespaceMalformed )
223+ } catch ( error ) {
224+ whitespaceCause = error
225+ }
226+ if ( ! whitespaceCause ) throw new Error ( "expected whitespace-heavy input to fail JSON parsing" )
227+ const whitespaceError = new InvalidToolInputError ( {
228+ toolName : "browser_execute" ,
229+ toolInput : whitespaceMalformed ,
230+ cause : new JSONParseError ( { text : whitespaceMalformed , cause : whitespaceCause } ) ,
231+ } )
232+ const boundedWhitespace = LLM . invalidToolCallInput ( "browser_execute" , whitespaceError . message )
233+ expect ( boundedWhitespace . length ) . toBeLessThanOrEqual ( Truncate . MAX_ERROR_CHARS )
234+ expect ( JSON . parse ( boundedWhitespace ) . error ) . toContain ( "characters omitted" )
235+ } )
236+ } )
237+
175238describe ( "session.llm.ai-sdk adapter" , ( ) => {
176239 type AISDKAdapterEvent = Parameters < typeof LLMAISDK . toLLMEvents > [ 1 ]
177240
0 commit comments