@@ -50,6 +50,16 @@ import { Truncate } from "@/tool/truncation"
5050// @ts -ignore
5151globalThis . AI_SDK_LOG_WARNINGS = false
5252
53+ const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
54+
55+ IMPORTANT:
56+ - You MUST call this tool exactly once at the end of your response
57+ - The input must be valid JSON matching the required schema
58+ - Complete all necessary research and tool calls BEFORE calling this tool
59+ - This tool provides your final answer - no further actions are taken after calling it`
60+
61+ const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`
62+
5363export namespace SessionPrompt {
5464 const log = Log . create ( { service : "session.prompt" } )
5565
@@ -96,6 +106,7 @@ export namespace SessionPrompt {
96106 . describe (
97107 "@deprecated tools and permissions have been merged, you can set permissions on the session itself now" ,
98108 ) ,
109+ format : MessageV2 . Format . optional ( ) ,
99110 system : z . string ( ) . optional ( ) ,
100111 variant : z . string ( ) . optional ( ) ,
101112 parts : z . array (
@@ -276,6 +287,11 @@ export namespace SessionPrompt {
276287
277288 using _ = defer ( ( ) => cancel ( sessionID ) )
278289
290+ // Structured output state
291+ // Note: On session resumption, state is reset but outputFormat is preserved
292+ // on the user message and will be retrieved from lastUser below
293+ let structuredOutput : unknown | undefined
294+
279295 let step = 0
280296 const session = await Session . get ( sessionID )
281297 while ( true ) {
@@ -589,6 +605,16 @@ export namespace SessionPrompt {
589605 messages : msgs ,
590606 } )
591607
608+ // Inject StructuredOutput tool if JSON schema mode enabled
609+ if ( lastUser . format ?. type === "json_schema" ) {
610+ tools [ "StructuredOutput" ] = createStructuredOutputTool ( {
611+ schema : lastUser . format . schema ,
612+ onSuccess ( output ) {
613+ structuredOutput = output
614+ } ,
615+ } )
616+ }
617+
592618 if ( step === 1 ) {
593619 SessionSummary . summarize ( {
594620 sessionID : sessionID ,
@@ -619,12 +645,19 @@ export namespace SessionPrompt {
619645
620646 await Plugin . trigger ( "experimental.chat.messages.transform" , { } , { messages : sessionMessages } )
621647
648+ // Build system prompt, adding structured output instruction if needed
649+ const system = [ ...( await SystemPrompt . environment ( model ) ) , ...( await InstructionPrompt . system ( ) ) ]
650+ const format = lastUser . format ?? { type : "text" }
651+ if ( format . type === "json_schema" ) {
652+ system . push ( STRUCTURED_OUTPUT_SYSTEM_PROMPT )
653+ }
654+
622655 const result = await processor . process ( {
623656 user : lastUser ,
624657 agent,
625658 abort,
626659 sessionID,
627- system : [ ... ( await SystemPrompt . environment ( model ) ) , ... ( await InstructionPrompt . system ( ) ) ] ,
660+ system,
628661 messages : [
629662 ...MessageV2 . toModelMessages ( sessionMessages , model ) ,
630663 ...( isLastStep
@@ -638,7 +671,33 @@ export namespace SessionPrompt {
638671 ] ,
639672 tools,
640673 model,
674+ toolChoice : format . type === "json_schema" ? "required" : undefined ,
641675 } )
676+
677+ // If structured output was captured, save it and exit immediately
678+ // This takes priority because the StructuredOutput tool was called successfully
679+ if ( structuredOutput !== undefined ) {
680+ processor . message . structured = structuredOutput
681+ processor . message . finish = processor . message . finish ?? "stop"
682+ await Session . updateMessage ( processor . message )
683+ break
684+ }
685+
686+ // Check if model finished (finish reason is not "tool-calls" or "unknown")
687+ const modelFinished = processor . message . finish && ! [ "tool-calls" , "unknown" ] . includes ( processor . message . finish )
688+
689+ if ( modelFinished && ! processor . message . error ) {
690+ if ( format . type === "json_schema" ) {
691+ // Model stopped without calling StructuredOutput tool
692+ processor . message . error = new MessageV2 . StructuredOutputError ( {
693+ message : "Model did not produce structured output" ,
694+ retries : 0 ,
695+ } ) . toObject ( )
696+ await Session . updateMessage ( processor . message )
697+ break
698+ }
699+ }
700+
642701 if ( result === "stop" ) break
643702 if ( result === "compact" ) {
644703 await SessionCompaction . create ( {
@@ -669,7 +728,8 @@ export namespace SessionPrompt {
669728 return Provider . defaultModel ( )
670729 }
671730
672- async function resolveTools ( input : {
731+ /** @internal Exported for testing */
732+ export async function resolveTools ( input : {
673733 agent : Agent . Info
674734 model : Provider . Model
675735 session : Session . Info
@@ -849,6 +909,36 @@ export namespace SessionPrompt {
849909 return tools
850910 }
851911
912+ /** @internal Exported for testing */
913+ export function createStructuredOutputTool ( input : {
914+ schema : Record < string , any >
915+ onSuccess : ( output : unknown ) => void
916+ } ) : AITool {
917+ // Remove $schema property if present (not needed for tool input)
918+ const { $schema, ...toolSchema } = input . schema
919+
920+ return tool ( {
921+ id : "StructuredOutput" as any ,
922+ description : STRUCTURED_OUTPUT_DESCRIPTION ,
923+ inputSchema : jsonSchema ( toolSchema as any ) ,
924+ async execute ( args ) {
925+ // AI SDK validates args against inputSchema before calling execute()
926+ input . onSuccess ( args )
927+ return {
928+ output : "Structured output captured successfully." ,
929+ title : "Structured Output" ,
930+ metadata : { valid : true } ,
931+ }
932+ } ,
933+ toModelOutput ( result ) {
934+ return {
935+ type : "text" ,
936+ value : result . output ,
937+ }
938+ } ,
939+ } )
940+ }
941+
852942 async function createUserMessage ( input : PromptInput ) {
853943 const agent = await Agent . get ( input . agent ?? ( await Agent . defaultAgent ( ) ) )
854944
@@ -870,6 +960,7 @@ export namespace SessionPrompt {
870960 agent : agent . name ,
871961 model,
872962 system : input . system ,
963+ format : input . format ,
873964 variant,
874965 }
875966 using _ = defer ( ( ) => InstructionPrompt . clear ( info . id ) )
0 commit comments