@@ -498,6 +498,231 @@ describe("afterAgent", () => {
498498 } ) ;
499499} ) ;
500500
501+ // ---------------------------------------------------------------------------
502+ // Auto-A2UI — middleware injects + executes generate_a2ui when the frontend
503+ // registered a catalog (surfaced into state["ag-ui"].a2ui_schema)
504+ // ---------------------------------------------------------------------------
505+ //
506+ // Contract: the developer passes nothing — using the middleware is enough.
507+ // generate_a2ui is advertised to the model only when an A2UI catalog is
508+ // present, is built from the agent's own (inferred) model, and is executed by
509+ // the middleware itself (it is never in the agent's static tool registry).
510+
511+ async function runWrapTool ( middleware : any , request : any ) {
512+ let received : any = null ;
513+ const handler = async ( req : any ) => {
514+ received = req ;
515+ return { content : "tool-ok" } as any ;
516+ } ;
517+ await middleware . wrapToolCall ( request , handler ) ;
518+ return received ;
519+ }
520+
521+ describe ( "auto-A2UI injection" , ( ) => {
522+ it ( "does NOT advertise generate_a2ui when the inject flag is absent" , async ( ) => {
523+ const request = makeRequest ( {
524+ state : { messages : [ ] , thread_id : "a2ui-off" } ,
525+ tools : [ { name : "backend" } ] ,
526+ } ) ;
527+
528+ const { received } = await runWrap ( copilotkitMiddleware , request ) ;
529+
530+ expect ( received . tools . map ( ( t : any ) => t . name ) ) . toEqual ( [ "backend" ] ) ;
531+ } ) ;
532+
533+ it ( "does NOT advertise generate_a2ui when a catalog is present but the flag is absent (opt-in)" , async ( ) => {
534+ const request = makeRequest ( {
535+ state : {
536+ messages : [ ] ,
537+ thread_id : "a2ui-noflag" ,
538+ "ag-ui" : { a2ui_schema : "<components/>" } ,
539+ } ,
540+ tools : [ { name : "backend" } ] ,
541+ } ) ;
542+
543+ const { received } = await runWrap ( copilotkitMiddleware , request ) ;
544+
545+ const names = received . tools . map ( ( t : any ) => t . name ) ;
546+ expect ( names ) . not . toContain ( "generate_a2ui" ) ;
547+ expect ( names ) . toContain ( "backend" ) ;
548+ } ) ;
549+
550+ it ( "advertises generate_a2ui (alongside existing tools) when the flag is on" , async ( ) => {
551+ const request = makeRequest ( {
552+ state : {
553+ messages : [ ] ,
554+ thread_id : "a2ui-on" ,
555+ "ag-ui" : { a2ui_schema : "<components/>" , inject_a2ui_tool : true } ,
556+ } ,
557+ tools : [ { name : "backend" } ] ,
558+ } ) ;
559+
560+ const { received } = await runWrap ( copilotkitMiddleware , request ) ;
561+
562+ const names = received . tools . map ( ( t : any ) => t . name ) ;
563+ expect ( names ) . toContain ( "backend" ) ;
564+ expect ( names ) . toContain ( "generate_a2ui" ) ;
565+ } ) ;
566+
567+ it ( "binds to the catalog from copilotkit.context when the flag is on (runtime-proxy path)" , async ( ) => {
568+ const request = makeRequest ( {
569+ state : {
570+ messages : [ ] ,
571+ thread_id : "a2ui-ctx" ,
572+ "ag-ui" : { inject_a2ui_tool : true } ,
573+ copilotkit : {
574+ context : [
575+ {
576+ description :
577+ "A2UI catalog capabilities: available catalog IDs and custom component definitions." ,
578+ value :
579+ "Available A2UI catalog:\n- declarative-gen-ui-catalog\n - Card: {...}\n - Metric: {...}" ,
580+ } ,
581+ ] ,
582+ } ,
583+ } ,
584+ tools : [ { name : "backend" } ] ,
585+ } ) ;
586+
587+ const { received } = await runWrap ( copilotkitMiddleware , request ) ;
588+
589+ const names = received . tools . map ( ( t : any ) => t . name ) ;
590+ expect ( names ) . toContain ( "backend" ) ;
591+ expect ( names ) . toContain ( "generate_a2ui" ) ;
592+ } ) ;
593+
594+ it ( "executes generate_a2ui via wrapToolCall using the inferred model" , async ( ) => {
595+ const state = {
596+ messages : [ ] ,
597+ thread_id : "a2ui-exec" ,
598+ "ag-ui" : { a2ui_schema : "<components/>" , inject_a2ui_tool : true } ,
599+ } ;
600+ // First the model call infers the model + stashes the built tool.
601+ await runWrap ( copilotkitMiddleware , makeRequest ( { state, tools : [ ] } ) ) ;
602+
603+ const received = await runWrapTool ( copilotkitMiddleware , {
604+ toolCall : { name : "generate_a2ui" , id : "1" , args : { } } ,
605+ tool : undefined ,
606+ state,
607+ runtime : { } ,
608+ } ) ;
609+
610+ expect ( received . tool ) . toBeDefined ( ) ;
611+ expect ( received . tool . name ) . toBe ( "generate_a2ui" ) ;
612+ } ) ;
613+
614+ it ( "leaves non-A2UI tool calls untouched" , async ( ) => {
615+ const state = {
616+ messages : [ ] ,
617+ thread_id : "a2ui-other" ,
618+ "ag-ui" : { a2ui_schema : "<components/>" , inject_a2ui_tool : true } ,
619+ } ;
620+ await runWrap ( copilotkitMiddleware , makeRequest ( { state, tools : [ ] } ) ) ;
621+
622+ const backendTool = { name : "backend" } ;
623+ const received = await runWrapTool ( copilotkitMiddleware , {
624+ toolCall : { name : "backend" , id : "1" , args : { } } ,
625+ tool : backendTool ,
626+ state,
627+ runtime : { } ,
628+ } ) ;
629+
630+ expect ( received . tool ) . toBe ( backendTool ) ;
631+ } ) ;
632+
633+ it ( "stops executing generate_a2ui after the run ends (afterAgent clears the bridge)" , async ( ) => {
634+ const state = {
635+ messages : [ ] ,
636+ thread_id : "a2ui-clean" ,
637+ "ag-ui" : { a2ui_schema : "<components/>" , inject_a2ui_tool : true } ,
638+ } ;
639+ await runWrap ( copilotkitMiddleware , makeRequest ( { state, tools : [ ] } ) ) ;
640+ copilotkitMiddleware . afterAgent ( state , { } as any ) ;
641+
642+ const received = await runWrapTool ( copilotkitMiddleware , {
643+ toolCall : { name : "generate_a2ui" , id : "1" , args : { } } ,
644+ tool : undefined ,
645+ state,
646+ runtime : { } ,
647+ } ) ;
648+
649+ expect ( received . tool ) . toBeUndefined ( ) ;
650+ } ) ;
651+
652+ // --- A2UI injectA2UITool flag (forwarded → ag-ui state) ------------------
653+
654+ it ( "does NOT advertise generate_a2ui when inject_a2ui_tool is false" , async ( ) => {
655+ const request = makeRequest ( {
656+ state : {
657+ messages : [ ] ,
658+ thread_id : "a2ui-optout" ,
659+ "ag-ui" : { a2ui_schema : "<components/>" , inject_a2ui_tool : false } ,
660+ } ,
661+ tools : [ { name : "backend" } ] ,
662+ } ) ;
663+
664+ const { received } = await runWrap ( copilotkitMiddleware , request ) ;
665+
666+ const names = received . tools . map ( ( t : any ) => t . name ) ;
667+ expect ( names ) . not . toContain ( "generate_a2ui" ) ;
668+ expect ( names ) . toContain ( "backend" ) ;
669+ } ) ;
670+
671+ it ( "advertises generate_a2ui when inject_a2ui_tool is true" , async ( ) => {
672+ const request = makeRequest ( {
673+ state : {
674+ messages : [ ] ,
675+ thread_id : "a2ui-optin" ,
676+ "ag-ui" : { a2ui_schema : "<components/>" , inject_a2ui_tool : true } ,
677+ } ,
678+ tools : [ { name : "backend" } ] ,
679+ } ) ;
680+
681+ const { received } = await runWrap ( copilotkitMiddleware , request ) ;
682+
683+ expect ( received . tools . map ( ( t : any ) => t . name ) ) . toContain ( "generate_a2ui" ) ;
684+ } ) ;
685+
686+ it ( "drops the runtime's render_a2ui when injecting our generate_a2ui" , async ( ) => {
687+ const request = makeRequest ( {
688+ state : {
689+ messages : [ ] ,
690+ thread_id : "a2ui-drop" ,
691+ "ag-ui" : { a2ui_schema : "<components/>" , inject_a2ui_tool : true } ,
692+ copilotkit : {
693+ actions : [ { name : "render_a2ui" } , { name : "fe_tool" } ] ,
694+ } ,
695+ } ,
696+ tools : [ ] ,
697+ } ) ;
698+
699+ const { received } = await runWrap ( copilotkitMiddleware , request ) ;
700+
701+ const names = received . tools . map ( ( t : any ) => t . name ) ;
702+ expect ( names ) . toContain ( "generate_a2ui" ) ;
703+ expect ( names ) . toContain ( "fe_tool" ) ;
704+ expect ( names ) . not . toContain ( "render_a2ui" ) ;
705+ } ) ;
706+
707+ it ( "does NOT double-inject when the agent already defines generate_a2ui" , async ( ) => {
708+ const request = makeRequest ( {
709+ state : {
710+ messages : [ ] ,
711+ thread_id : "a2ui-dup" ,
712+ "ag-ui" : { a2ui_schema : "<components/>" , inject_a2ui_tool : true } ,
713+ } ,
714+ tools : [ { name : "generate_a2ui" } ] ,
715+ } ) ;
716+
717+ const { received } = await runWrap ( copilotkitMiddleware , request ) ;
718+
719+ const count = received . tools . filter (
720+ ( t : any ) => t . name === "generate_a2ui" ,
721+ ) . length ;
722+ expect ( count ) . toBe ( 1 ) ;
723+ } ) ;
724+ } ) ;
725+
501726// ---------------------------------------------------------------------------
502727// zodState — Standard-Schema JSON-schema augmentation
503728// ---------------------------------------------------------------------------
0 commit comments