@@ -43,6 +43,7 @@ const HUMAN_IN_LOOP_TOOLS = new Set([
4343 "manage_streams" ,
4444 "manage_keys" ,
4545 "manage_subgraphs" ,
46+ "manage_workflows" ,
4647 "deploy_workflow" ,
4748 "edit_workflow" ,
4849 "rollback_workflow" ,
@@ -228,13 +229,16 @@ export function ToolPartRenderer({
228229 const input = part . input as {
229230 action : string ;
230231 targets : Array < { id ?: string ; name : string ; reason ?: string } > ;
232+ triggerInput ?: string ;
231233 } ;
232234 const resourceType =
233235 toolName === "manage_keys"
234236 ? "keys"
235237 : toolName === "manage_subgraphs"
236238 ? "subgraphs"
237- : "streams" ;
239+ : toolName === "manage_workflows"
240+ ? "workflows"
241+ : "streams" ;
238242 return (
239243 < >
240244 < ToolCallIndicator
@@ -250,6 +254,18 @@ export function ToolPartRenderer({
250254 reason : t . reason ,
251255 } ) ) }
252256 onConfirm = { async ( ) => {
257+ if ( toolName === "manage_workflows" ) {
258+ const result = await executeManageWorkflows (
259+ input . action ,
260+ input . targets ,
261+ input . triggerInput ,
262+ ) ;
263+ addToolOutput ( {
264+ toolCallId : part . toolCallId ,
265+ output : result ,
266+ } ) ;
267+ return ;
268+ }
253269 await executeAction ( toolName , input . action , input . targets ) ;
254270 addToolOutput ( {
255271 toolCallId : part . toolCallId ,
@@ -380,10 +396,17 @@ function renderOutputCard(toolName: string, output: Record<string, unknown>) {
380396
381397 case "manage_streams" :
382398 case "manage_keys" :
383- case "manage_subgraphs" : {
399+ case "manage_subgraphs" :
400+ case "manage_workflows" : {
384401 const msg = ( output as { message ?: string } ) . message ;
385402 if ( ( output as { confirmed ?: boolean } ) . confirmed === false ) {
386- return < SuccessBanner message = { msg ?? "Action cancelled" } /> ;
403+ return (
404+ < SuccessBanner tone = "info" message = { msg ?? "Action cancelled" } />
405+ ) ;
406+ }
407+ const errored = ( output as { error ?: string } ) . error ;
408+ if ( errored ) {
409+ return < SuccessBanner tone = "error" message = { errored } /> ;
387410 }
388411 return < SuccessBanner message = { msg ?? "Action completed" } /> ;
389412 }
@@ -602,6 +625,137 @@ async function executeAction(
602625 await Promise . allSettled ( calls ) ;
603626}
604627
628+ /**
629+ * Handler for `manage_workflows` HIL confirms. Split from the generic
630+ * `executeAction` because workflow operations need structured response
631+ * handling:
632+ * - `trigger` returns a runId we want to pass back to the agent so it can
633+ * tail the run in a follow-up step.
634+ * - Any non-2xx upstream response needs to surface as a chat error rather
635+ * than being silently swallowed by `Promise.allSettled`.
636+ */
637+ async function executeManageWorkflows (
638+ action : string ,
639+ targets : Array < { id ?: string ; name : string ; reason ?: string } > ,
640+ triggerInput ?: string ,
641+ ) : Promise < {
642+ confirmed : boolean ;
643+ ok : boolean ;
644+ action : string ;
645+ name ?: string ;
646+ runId ?: string ;
647+ message ?: string ;
648+ error ?: string ;
649+ } > {
650+ const first = targets [ 0 ] ;
651+ if ( ! first ) {
652+ return {
653+ confirmed : true ,
654+ ok : false ,
655+ action,
656+ error : "No workflow targets provided" ,
657+ } ;
658+ }
659+
660+ try {
661+ if ( action === "trigger" ) {
662+ let inputBody : Record < string , unknown > = { } ;
663+ if ( triggerInput ) {
664+ try {
665+ const parsed = JSON . parse ( triggerInput ) ;
666+ if ( parsed && typeof parsed === "object" ) {
667+ inputBody = { input : parsed } ;
668+ }
669+ } catch {
670+ return {
671+ confirmed : true ,
672+ ok : false ,
673+ action,
674+ name : first . name ,
675+ error : "triggerInput was not valid JSON" ,
676+ } ;
677+ }
678+ }
679+ const res = await fetch ( `/api/workflows/${ first . name } /trigger` , {
680+ method : "POST" ,
681+ headers : { "Content-Type" : "application/json" } ,
682+ credentials : "same-origin" ,
683+ body : JSON . stringify ( inputBody ) ,
684+ } ) ;
685+ const body = ( await res . json ( ) . catch ( ( ) => ( { } ) ) ) as {
686+ runId ?: string ;
687+ error ?: string ;
688+ } ;
689+ if ( ! res . ok ) {
690+ return {
691+ confirmed : true ,
692+ ok : false ,
693+ action,
694+ name : first . name ,
695+ error : body . error ?? `HTTP ${ res . status } ` ,
696+ } ;
697+ }
698+ return {
699+ confirmed : true ,
700+ ok : true ,
701+ action,
702+ name : first . name ,
703+ runId : body . runId ,
704+ message : body . runId
705+ ? `Triggered ${ first . name } — run ${ body . runId } `
706+ : `Triggered ${ first . name } ` ,
707+ } ;
708+ }
709+
710+ const path =
711+ action === "pause"
712+ ? `/api/workflows/${ first . name } /pause`
713+ : action === "resume"
714+ ? `/api/workflows/${ first . name } /resume`
715+ : action === "delete"
716+ ? `/api/workflows/${ first . name } `
717+ : null ;
718+ if ( ! path ) {
719+ return {
720+ confirmed : true ,
721+ ok : false ,
722+ action,
723+ error : `Unknown workflow action: ${ action } ` ,
724+ } ;
725+ }
726+ const res = await fetch ( path , {
727+ method : action === "delete" ? "DELETE" : "POST" ,
728+ credentials : "same-origin" ,
729+ headers : { "Content-Type" : "application/json" } ,
730+ } ) ;
731+ if ( ! res . ok ) {
732+ const body = ( await res . json ( ) . catch ( ( ) => ( { } ) ) ) as { error ?: string } ;
733+ return {
734+ confirmed : true ,
735+ ok : false ,
736+ action,
737+ name : first . name ,
738+ error : body . error ?? `HTTP ${ res . status } ` ,
739+ } ;
740+ }
741+ return {
742+ confirmed : true ,
743+ ok : true ,
744+ action,
745+ name : first . name ,
746+ message : `${ first . name } ${ action } d` ,
747+ } ;
748+ } catch ( err ) {
749+ return {
750+ confirmed : true ,
751+ ok : false ,
752+ action,
753+ name : first . name ,
754+ error : err instanceof Error ? err . message : String ( err ) ,
755+ } ;
756+ }
757+ }
758+
605759type BundleWorkflowResult = {
606760 ok : boolean ;
607761 name ?: string ;
0 commit comments