From d29bd87deda06be81f1210ff5194271314f825a2 Mon Sep 17 00:00:00 2001 From: Lucas Woodward <31957045+SketchingDev@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:04:22 +0100 Subject: [PATCH] Update skill --- servers/genesys-cloud-architect-mcp.js | 2 +- skills/interpret-flow-ir/SKILL.md | 61 +++++++++++++++----------- src/mcp-server/tools/flow-ir.ts | 4 +- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/servers/genesys-cloud-architect-mcp.js b/servers/genesys-cloud-architect-mcp.js index 06b4bce..c346544 100644 --- a/servers/genesys-cloud-architect-mcp.js +++ b/servers/genesys-cloud-architect-mcp.js @@ -155,7 +155,7 @@ ${r.join(` Validation warnings: ${s.warnings.join(` `)}`),l({content:[{type:"text",text:f.join(` -`)+m}]})}else{let f=s?.error??`Deploy runner exited with code ${h}`;l({isError:!0,content:[{type:"text",text:`Deploy failed: ${f}${m}`}]})}})})}});var vt=t=>`${t}::start`,ms=(t,e)=>`${t}::${e}`;function fI(t,e,i,n){let a=new Set(e.backEdges.map(h=>`${h.from} ${h.to}`)),r=new Map,s=new Map,o=new Map,l=new Map;for(let h of t.nodes.keys())r.set(h,[]),s.set(h,new Set),o.set(h,[]),l.set(h,new Set);for(let h of t.edges){let g=a.has(`${h.from} ${h.to}`),m=r.get(h.to);if(m){let w=`${h.from} ${h.label??""} ${g}`,b=s.get(h.to);b&&!b.has(w)&&(b.add(w),m.push({id:h.from,label:h.label,backEdge:g}))}let f=o.get(h.from);if(f){let w=`${h.to} ${h.label??""} ${g}`,b=l.get(h.from);b&&!b.has(w)&&(b.add(w),f.push({id:h.to,label:h.label,backEdge:g}))}}let u=-1;for(let h of e.order.values())h>u&&(u=h);let c=u+1,p=[];for(let[h,g]of t.nodes){let m=e.order.has(h),f=m?e.order.get(h):c++;p.push({id:h,kind:g.kind,actionType:g.actionType,label:g.label,description:g.description,predecessors:r.get(h)??[],successors:o.get(h)??[],order:f,taskId:g.taskId,taskName:g.taskName,reachable:m,terminal:g.terminal})}p.sort((h,g)=>h.order-g.order);let d=i.taskOrder.flatMap(h=>{let g=i.tasks.get(h);return g?[{id:h,name:g.name,reusable:i.reusableTaskIds.has(h)}]:[]});return{flowName:i.flowName,flowType:i.flowType,entryTaskId:i.entryTaskId,reachabilityIsComplete:n,tasks:d,nodes:p}}function b7(t,e){let i=t;for(let n of e.split(".")){if(i==null||typeof i!="object")return;i=i[n]}return typeof i=="string"&&i.length>0?i:void 0}function it(t,e){for(let i of e){let n=b7(t,i);if(n!==void 0)return n}}function wI(t){let e=t.expression;if(e==null||typeof e!="object")return;let{text:i,type:n}=e;if(typeof i=="string"&&i.trim().length>0)return i;if(typeof n=="string"&&n.length>0)return n}function Hb(t,e){let i=t.find(n=>n.outputId===e);return i?(i.terminal=!0,t):[{outputId:e,label:Ib(e),synthetic:!0,terminal:!0},...t]}var Sl={isTerminal:()=>!0,outputs:()=>[]},vI={isTerminal:()=>!0,describe:t=>it(t,["transferTo","group.text","flowName","externalNumber.text","user.text","transferTarget.text","transferTargetGroup.text","queues.0.text"]),outputs:t=>Hb(fs(t).map(er),"__SUCCESS__")},y7={isTerminal:()=>!1,outputs:t=>{let e=t.path;if(!e||typeof e!="object")return[];let i=er(e,0);return i.outputId="__LOOP__",typeof e.label=="string"&&e.label.length>0||(i.label="Loop"),[i]}},P7={outputs:t=>Hb(fs(t).map(er),"__SUCCESS__")},j7={isTerminal:()=>!0,describe:t=>it(t,["flowName"]),outputs:t=>Hb(fs(t).map(er),"__DISCONNECT__")},S7={outputs:t=>{let e=fs(t);return e.length>0?[er(e[0],0)]:[]}},et=t=>({describe:t}),Yp={DisconnectAction:Sl,EndFlowAction:Sl,EndTaskAction:Sl,EndStateAction:Sl,ExitBotFlowAction:Sl,LoopAction:y7,ProcessVoicemailInputAction:P7,CallBotFlowAction:j7,AskForNLUNextIntentAction:S7,WaitForInputAction:{},CallTaskAction:{},TaskAction:{},TransferMenuAction:{},TransferTaskAction:{},LoopUntilAction:{},DecisionAction:et(wI),SwitchAction:et(wI),DataAction:et(t=>it(t,["actionName"])),DataTableLookupAction:et(t=>it(t,["datatableName"])),CallCommonModuleAction:et(t=>it(t,["flowName"])),CallBotConnectorAction:et(t=>it(t,["botName"])),CallDigitalBotFlowAction:et(t=>it(t,["flowName"])),AddFlowMilestoneAction:et(t=>it(t,["milestoneName"])),ScreenPopAction:et(t=>it(t,["inputs.0.value.text"])),PlayAudioAction:et(t=>it(t,["prompts.defaultAudio.text"])),UpdateVariableAction:{},SetAttributesAction:{},GetAttributesAction:{},EvaluateScheduleAction:{},CollectInputAction:{},GetResponseAction:{},AskForBooleanAction:{},AskForSlotAction:{},CommunicateAction:{},DigitalMenuAction:{},SendResponseAction:{},TranscriptionAction:{},FindQueueAction:{},FindUserPromptAction:{},SetWhisperAudio:{},BridgeServerAction:{}};function Xp(t){return typeof t=="string"&&t.length>0}function fs(t){let e=[];if(Array.isArray(t.paths))for(let i of t.paths)i&&typeof i=="object"&&e.push(i);return t.path&&typeof t.path=="object"&&e.push(t.path),e}function Ib(t){let e=t.replace(/^__/,"").replace(/__$/,"");if(e.length===0)return t;let i=e.toLowerCase();return i.charAt(0).toUpperCase()+i.slice(1)}function er(t,e){let i=t.outputId??`__OUTPUT_${e}__`,n=typeof t.label=="string"&&t.label.length>0?t.label:Ib(i),a={outputId:i,label:n,nextActionId:t.nextActionId,synthetic:!1,terminal:!1};return t.enabled===!1&&(a.disabled=!0),a}var O7=["taskReference","menuReference"],CI={describe:()=>{},outputs:t=>fs(t).map(er),isTerminal:()=>!1,referencedTaskIds:t=>{let e=[];for(let i of O7){let n=t[i];typeof n=="string"&&n.length>0&&e.push(n)}return e}},T7=new Set(["TransferMenuAction","TransferTaskAction"]);function AI(t){return typeof t=="string"&&t.startsWith("Transfer")&&!T7.has(t)}function bI(t){return t?t in Yp||AI(t):!1}function yI(t){let e=AI(t)?{...CI,...vI}:CI,i=t&&t in Yp?Yp[t]:void 0;return i?{...e,...i}:e}function*ed(t){for(let e of t.taskOrder){let i=t.tasks.get(e);if(i)for(let n of i.orderedActions)typeof n.id=="string"&&(yield{taskId:e,task:i,action:n,actionId:n.id,handler:yI(n.__type)})}}function PI(t){let e=[],i=[];for(let n of t.taskOrder){let a=t.tasks.get(n);if(a){Xp(a.startAction)&&e.push({from:vt(n),to:a.startAction,kind:"fallthrough"});for(let r of a.menuChoices)e.push({from:vt(n),to:r.actionId,kind:"reference",label:r.label})}}for(let{taskId:n,actionId:a,action:r,handler:s}of ed(t)){let o=s.isTerminal(r);Xp(r.nextAction)&&!o&&e.push({from:a,to:r.nextAction,kind:"fallthrough"});for(let l of s.outputs(r)){let u=ms(a,l.outputId);e.push({from:a,to:u,kind:"branch",label:l.label}),Xp(l.nextActionId)&&!l.terminal&&e.push({from:u,to:l.nextActionId,kind:l.outputId==="__LOOP__"?"loop":"branch"})}for(let l of s.referencedTaskIds(r)){let u=t.tasks.get(l);u?e.push({from:a,to:vt(l),kind:"reference",label:u.name}):i.push({code:"UNRESOLVED_REFERENCE",message:`Reference from ${a} to task ${l} is unresolvable.`,nodeId:a,taskId:n})}}return{edges:e,warnings:i}}function jI(t,e,i){let n=new Map;for(let u of t)n.set(u.id,u);let a=new Map;for(let u of n.keys())a.set(u,[]);let r=[],s=[],o=[];for(let u of e){let c=n.has(u.from),p=n.has(u.to);if(!c||!p){let d=c?`unknown target node "${u.to}"`:`unknown source node "${u.from}"`;s.push({...u,reason:d}),o.push({code:"DROPPED_EDGE",message:`Dropped ${u.kind} edge ${u.from} \u2192 ${u.to}: ${d}.`});continue}r.push(u),a.get(u.from)?.push(u.to)}let l=i.taskOrder.map(vt).filter(u=>n.has(u));return{graph:{nodes:n,edges:r,adjacency:a,roots:l,dropped:s},warnings:o}}function x7(t,e,i){let n=[],a=[];if(Array.isArray(t.actionList))for(let r of t.actionList)r&&typeof r=="object"&&n.push(r);if(Array.isArray(t.menuChoiceList))for(let r of t.menuChoiceList){let s=r?.action;if(!s||typeof s!="object")continue;if(typeof s.id!="string"){i.push({code:"MISSING_ACTION_ID",message:`Menu-choice action of type "${s.__type??"(missing)"}" in task ${e} has no id and was skipped.`,taskId:e});continue}n.push(s);let o=typeof r.name=="string"&&r.name.length>0?r.name:s.name??s.id;a.push({actionId:s.id,label:o})}return{orderedActions:n,menuChoices:a}}function M7(t){if(typeof t=="string")return t;if(t&&typeof t=="object"){let e=t.id;if(typeof e=="string")return e}}function SI(t){let e=[],i=new Map,n=Array.isArray(t.flowSequenceItemList)?t.flowSequenceItemList:[];for(let p of n){if(!p||typeof p!="object"||typeof p.id!="string")continue;let d=typeof p.name=="string"&&p.name.length>0?p.name:p.id,{orderedActions:h,menuChoices:g}=x7(p,p.id,e);i.set(p.id,{id:p.id,name:d,startAction:typeof p.startAction=="string"?p.startAction:void 0,orderedActions:h,menuChoices:g})}let a=new Set;for(let{id:p,orderedActions:d}of i.values())for(let h of d){if(typeof h.id!="string"){e.push({code:"MISSING_ACTION_ID",message:`Action of type "${h.__type??"(missing)"}" in task ${p} has no id and was skipped.`,taskId:p});continue}a.has(h.id)&&e.push({code:"DUPLICATE_ACTION_ID",message:`Duplicate action id "${h.id}" in task ${p}; the later definition wins.`,nodeId:h.id,taskId:p}),a.add(h.id)}let r=[...i.keys()],s=t.initialSequence,o=typeof s=="string"&&i.has(s),l=o?[s,...r.filter(p=>p!==s)]:r;typeof s=="string"&&s.length>0&&!o&&e.push({code:"UNRESOLVED_INITIAL_SEQUENCE",message:`initialSequence "${s}" matches no task; using declaration order.`});let u=new Set,c=t.uiMetaData?.task;if(Array.isArray(c))for(let p of c){let d=M7(p);d!==void 0&&u.add(d)}return{flowName:typeof t.name=="string"?t.name:"",flowType:typeof t.type=="string"?t.type:"",tasks:i,taskOrder:l,entryTaskId:o?s:void 0,reusableTaskIds:u,warnings:e}}var E7=new Set(["AskForNLUNextIntentAction","WaitForInputAction"]);function OI(t){let e=[],i=[];for(let n of t.taskOrder){let a=t.tasks.get(n);a&&e.push({id:vt(n),kind:"task-start",label:a.name,taskId:n,taskName:a.name,terminal:!1})}for(let{taskId:n,task:a,action:r,actionId:s,handler:o}of ed(t)){let l=r.__type,u=typeof r.name=="string"&&r.name.length>0?r.name:l??s;e.push({id:s,kind:"action",actionType:l,label:u,description:o.describe(r),taskId:n,taskName:a.name,terminal:o.isTerminal(r)}),bI(l)||i.push({code:"UNKNOWN_ACTION_TYPE",message:`Unknown action __type "${l??"(missing)"}" \u2014 handled generically.`,nodeId:s,taskId:n}),E7.has(l??"")&&i.push({code:"UNRESOLVED_INTENT_FANOUT",message:`Intent fan-out for ${s} is not resolved: per-intent routing (nluMetaData) is not modelled.`,nodeId:s,taskId:n});for(let c of o.outputs(r))e.push({id:ms(s,c.outputId),kind:"branch-output",label:c.label,taskId:n,taskName:a.name,terminal:c.terminal}),c.disabled&&i.push({code:"DISABLED_BRANCH",message:`Output ${c.outputId} of ${s} is disabled.`,nodeId:ms(s,c.outputId),taskId:n})}return{nodes:e,warnings:i}}function Rb(t){let e=new Map;for(let s of t.nodes.keys())e.set(s,0);let i=new Map,n=[],a=0;for(let s of t.roots){if(e.get(s)!==0)continue;e.set(s,1),i.set(s,a++);let o=[{node:s,index:0}];for(;o.length>0;){let l=o[o.length-1],u=t.adjacency.get(l.node)??[];if(l.indexp.code==="UNRESOLVED_INTENT_FANOUT");return{ok:!0,ir:fI(s,l,e,u),graph:s,warnings:[...e.warnings,...n,...r,...o]}}catch(e){return{ok:!1,error:{code:"INTERNAL",message:e instanceof Error?e.message:String(e)}}}}function k7(t,e){if(Array.isArray(t.actionList))for(let i of t.actionList)i&&typeof i=="object"&&e(i);if(Array.isArray(t.menuChoiceList))for(let i of t.menuChoiceList){if(!i||typeof i!="object")continue;let n=i.action;!n||typeof n!="object"||e(n,q7(i))}}function q7(t){let e={};return typeof t.id=="string"&&(e.id=t.id),typeof t.name=="string"&&(e.name=t.name),typeof t.digit=="string"&&(e.digit=t.digit),e}function _7(t){if(t===null||typeof t!="object"||Array.isArray(t))return[];let e=t.flowSequenceItemList;return Array.isArray(e)?e:[]}function Db(t,e){let i=[],n=new Set;for(let o of Array.isArray(e)?e:[])typeof o=="string"&&!n.has(o)&&(n.add(o),i.push(o));if(i.length===0)return{found:[],notFound:[]};let a=new Map;for(let o of _7(t)){if(!o||typeof o!="object"||typeof o.id!="string")continue;let l=o.id,u=typeof o.name=="string"&&o.name.length>0?o.name:l;k7(o,(c,p)=>{if(typeof c.id!="string"||!n.has(c.id))return;let d={actionId:c.id,action:c,taskId:l,taskName:u};p&&(d.menuChoice=p);let h=a.get(c.id);h?h.push(d):a.set(c.id,[d])})}let r=[],s=[];for(let o of i){let l=a.get(o);l?r.push(...l):s.push(o)}return{found:r,notFound:s}}var TI=25;function H7(t){let e=[],i=new Map,n=!1;for(let a of t){let r=a.split("::")[0]||a;r!==a&&(n=!0),i.has(r)||(i.set(r,a),e.push(r))}return{lookupIds:e,requestedFor:i,strippedAny:n}}var I7={flowId:We.string().min(1).describe("The Genesys Cloud Architect flow ID"),actionIds:We.array(We.string().min(1)).min(1).max(TI).describe(`Action GUIDs, taken from the \`id\` of flow_ir nodes whose \`kind\` is "action". Batch every action of interest into one call rather than calling this tool repeatedly. Branch-output ids of the form \`::\` are also accepted; the suffix is stripped and the underlying action is returned. Maximum ${TI} ids.`)},xI=({architectApi:t})=>({config:{description:"Retrieves the raw Architect configuration of specific actions in a Genesys Cloud Architect flow: decision expressions, prompt text, data action inputs and outputs, and any other per-action settings that the flow_ir tool omits. Use flow_ir first to find action ids and to understand the control flow, then use this tool to find out what an action actually does. Do not use the raw fields returned here to make control-flow claims: flow_ir has already resolved those, and its answer accounts for branches the raw configuration does not reflect.",annotations:{title:"Flow Action",readOnlyHint:!0,destructiveHint:!1},inputSchema:I7},handler:async({flowId:e,actionIds:i})=>{let n;try{n=await t.getFlowLatestconfiguration(e)}catch{return{isError:!0,content:[{type:"text",text:`Flow "${e}" not found or not accessible.`}]}}let{lookupIds:a,requestedFor:r,strippedAny:s}=H7(i),o=Db(n,a),l=c=>({...c,actionId:r.get(c.actionId)??c.actionId}),u=[];return s&&u.push("Some requested ids were flow_ir synthetic node ids of the form ::; the suffix was stripped and the underlying action looked up, then reported under the id as requested. A ::start id identifies a task rather than an action, so it never matches."),o.notFound.length>0&&u.push("Ids in notFound are absent from this flow's latest configuration. The flow may have been redeployed since flow_ir was called, so re-run flow_ir and re-join rather than concluding those actions no longer exist."),{content:[{type:"text",text:JSON.stringify({flowId:e,found:o.found.map(l),notFound:o.notFound.map(c=>r.get(c)??c),...u.length>0?{notes:u}:{}})}]}}});function MI(t){let e=t.toUpperCase();return e.endsWith("FLOW")?e:`${e}FLOW`}function R7(t,e){let i={};for(let n of e){let a=n.type??"UNKNOWN";i[a]||(i[a]=[]),i[a].push({id:n.id??"",name:n.name??"",...n.version?{version:n.version}:{},deleted:n.deleted??!1,updated:n.updated??!1})}return{flow:{id:t.id??"",name:t.name,type:MI(t.type??""),version:t.publishedVersion?.commitVersion??"1"},dependencies:i}}var z7={flowId:We.string().min(1).describe("The Genesys Cloud Architect flow ID")},EI=({architectApi:t})=>({config:{description:"Retrieves all dependencies consumed by a Genesys Cloud Architect flow. Returns the flow metadata and its dependencies grouped by type.",annotations:{title:"Flow Dependencies",readOnlyHint:!0,destructiveHint:!1},inputSchema:z7},handler:async({flowId:e})=>{try{let i;try{i=await t.getFlow(e)}catch{return{isError:!0,content:[{type:"text",text:`Flow "${e}" not found.`}]}}let n=MI(i.type??""),a=i.publishedVersion?.commitVersion??"1",r=[],s=1;for(;;){let l=await t.getArchitectDependencytrackingConsumedresources(i.id,a,n,{pageSize:100,pageNumber:s});if(l.entities&&r.push(...l.entities),!l.nextUri)break;s++}let o=R7(i,r);return{content:[{type:"text",text:JSON.stringify(o,null,2)}]}}catch(i){return{isError:!0,content:[{type:"text",text:`Failed to retrieve flow dependencies: ${i instanceof Error?i.message:String(i)}`}]}}}});function D7(t,e){let i=t.find(r=>r.id===e);if(i)return{match:i};let n=e.toLowerCase(),a=t.filter(r=>r.name.toLowerCase()===n);return a.length>1?{ambiguous:a}:a[0]?{match:a[0]}:void 0}var G7={flowId:We.string().min(1).describe("The Genesys Cloud Architect flow ID"),task:We.string().min(1).optional().describe("Optional. Restrict the returned nodes to a single task, by task id or task name (case-insensitive). Use this to explore a large flow one task at a time. The full task list is always returned, and a node's predecessors may reference nodes in other tasks, which will not appear in the filtered node list.")},kI=({architectApi:t})=>({config:{description:"Retrieves the intermediate representation (IR) of a Genesys Cloud Architect flow: a flat, ordered list of its actions with the branches connecting them. Use this to understand what an existing flow does. It answers what follows an action, which branch leads where, which actions are unreachable, and how tasks call each other.",annotations:{title:"Flow IR",readOnlyHint:!0,destructiveHint:!1},inputSchema:G7},handler:async({flowId:e,task:i})=>{let n;try{n=await t.getFlowLatestconfiguration(e)}catch{return{isError:!0,content:[{type:"text",text:`Flow "${e}" not found or not accessible.`}]}}let a=zb(n);if(!a.ok)return{isError:!0,content:[{type:"text",text:`Failed to parse flow "${e}" (${a.error.code}): ${a.error.message}`}]};let r=a.ir;if(typeof i=="string"){let s=D7(r.tasks,i);if(!s){let o=r.tasks.map(l=>l.name).join(", ");return{isError:!0,content:[{type:"text",text:`No task matching "${i}" in flow "${e}". Available tasks: ${o||"(none)"}.`}]}}if("ambiguous"in s){let o=s.ambiguous.map(l=>`"${l.name}" (id: ${l.id})`).join(", ");return{isError:!0,content:[{type:"text",text:`Task name "${i}" is ambiguous in flow "${e}". It matches ${o}. Retry with the task id.`}]}}r={...r,nodes:r.nodes.filter(o=>o.taskId===s.match.id)}}return{content:[{type:"text",text:JSON.stringify({flowId:e,ir:r,warnings:a.warnings})}]}}});var Gb=new Map;function $7(t){let{id:e,previousTurn:i,...n}=t;return n}async function qI(t,e,i){let n=[],a=i;for(;n.push($7(a)),Gb.set(e,a.id),a.nextActionType==="NoOp";)a=await t.postTextbotsBotflowsSessionTurns(e,{previousTurn:{id:a.id},inputEventType:"NoOp",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:""}}]}});let r=a.nextActionType;return(r==="Disconnect"||r==="Exit")&&Gb.delete(e),{content:[{type:"text",text:JSON.stringify({sessionId:e,turns:n},null,2)}]}}var N7={flowId:We.string().optional().describe("The bot flow ID to test. Required when starting a new session."),sessionId:We.string().optional().describe("Session ID from a previous call. Required when continuing an existing conversation."),message:We.string().optional().describe("User message to send to the bot. Required when continuing a session.")},_I=({textbotsApi:t})=>({config:{description:"Tests a deployed Genesys Cloud Architect Bot Flow and Digital Bot Flow by simulating a text conversation. To start: provide flowId. To continue: provide the returned sessionId and a message. Turn tracking is managed server-side \u2014 only the sessionId is needed between calls. Returns { sessionId, turns[] } where each turn contains the raw API response (prompts with segments, outputLanguage, nextActionType, and action-specific data like modeConstraints or outputData). Multiple turns are returned when the flow sends consecutive messages (NoOp turns are automatically drained). nextActionType values: WaitForInput (send another message), Disconnect/Exit (conversation ended).",annotations:{title:"Test Bot Flow",readOnlyHint:!1,destructiveHint:!1},inputSchema:N7},handler:async({flowId:e,sessionId:i,message:n})=>{try{if(i&&e)return{isError:!0,content:[{type:"text",text:"Provide either flowId (to start) or sessionId (to continue), not both."}]};if(!i&&!e)return{isError:!0,content:[{type:"text",text:"Provide flowId to start a new session or sessionId to continue an existing one."}]};if(e){let s=await t.postTextbotsBotflowsSessions({flow:{id:e},externalSessionId:"",inputData:{variables:{}},channel:{inputModes:["Text"],outputModes:["Text"],name:"Messaging",userAgent:{name:"GenesysWebWidget"}},language:""}),o=await t.postTextbotsBotflowsSessionTurns(s.id,{inputEventType:"NoOp",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:""}}]}});return qI(t,s.id,o)}if(!i||!n)return{isError:!0,content:[{type:"text",text:"A message is required when continuing an existing session."}]};let a=Gb.get(i);if(!a)return{isError:!0,content:[{type:"text",text:`No active session found for ID "${i}". It may have expired or already ended. Start a new session with a flowId.`}]};let r=await t.postTextbotsBotflowsSessionTurns(i,{previousTurn:{id:a},inputEventType:"UserInput",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:n}}]}});return qI(t,i,r)}catch(a){return{isError:!0,content:[{type:"text",text:`Bot flow test failed: ${a instanceof Error?a.message:JSON.stringify(a)}`}]}}}});var $b=We.object({GENESYS_REGION:We.string().min(1),GENESYS_CLIENT_ID:We.string().min(1),GENESYS_CLIENT_SECRET:We.string().min(1),DEPLOY_SCRIPT_PATH:We.string().min(1),PREVENT_LOGIN:We.enum(["TRUE","FALSE"]).default("FALSE").transform(t=>t==="TRUE")}).safeParse(process.env);if(!$b.success){let t=$b.error.issues.map(e=>e.path[0]).join(` +`)+m}]})}else{let f=s?.error??`Deploy runner exited with code ${h}`;l({isError:!0,content:[{type:"text",text:`Deploy failed: ${f}${m}`}]})}})})}});var vt=t=>`${t}::start`,ms=(t,e)=>`${t}::${e}`;function fI(t,e,i,n){let a=new Set(e.backEdges.map(h=>`${h.from} ${h.to}`)),r=new Map,s=new Map,o=new Map,l=new Map;for(let h of t.nodes.keys())r.set(h,[]),s.set(h,new Set),o.set(h,[]),l.set(h,new Set);for(let h of t.edges){let g=a.has(`${h.from} ${h.to}`),m=r.get(h.to);if(m){let w=`${h.from} ${h.label??""} ${g}`,b=s.get(h.to);b&&!b.has(w)&&(b.add(w),m.push({id:h.from,label:h.label,backEdge:g}))}let f=o.get(h.from);if(f){let w=`${h.to} ${h.label??""} ${g}`,b=l.get(h.from);b&&!b.has(w)&&(b.add(w),f.push({id:h.to,label:h.label,backEdge:g}))}}let u=-1;for(let h of e.order.values())h>u&&(u=h);let c=u+1,p=[];for(let[h,g]of t.nodes){let m=e.order.has(h),f=m?e.order.get(h):c++;p.push({id:h,kind:g.kind,actionType:g.actionType,label:g.label,description:g.description,predecessors:r.get(h)??[],successors:o.get(h)??[],order:f,taskId:g.taskId,taskName:g.taskName,reachable:m,terminal:g.terminal})}p.sort((h,g)=>h.order-g.order);let d=i.taskOrder.flatMap(h=>{let g=i.tasks.get(h);return g?[{id:h,name:g.name,reusable:i.reusableTaskIds.has(h)}]:[]});return{flowName:i.flowName,flowType:i.flowType,entryTaskId:i.entryTaskId,reachabilityIsComplete:n,tasks:d,nodes:p}}function b7(t,e){let i=t;for(let n of e.split(".")){if(i==null||typeof i!="object")return;i=i[n]}return typeof i=="string"&&i.length>0?i:void 0}function it(t,e){for(let i of e){let n=b7(t,i);if(n!==void 0)return n}}function wI(t){let e=t.expression;if(e==null||typeof e!="object")return;let{text:i,type:n}=e;if(typeof i=="string"&&i.trim().length>0)return i;if(typeof n=="string"&&n.length>0)return n}function Hb(t,e){let i=t.find(n=>n.outputId===e);return i?(i.terminal=!0,t):[{outputId:e,label:Ib(e),synthetic:!0,terminal:!0},...t]}var Sl={isTerminal:()=>!0,outputs:()=>[]},vI={isTerminal:()=>!0,describe:t=>it(t,["transferTo","group.text","flowName","externalNumber.text","user.text","transferTarget.text","transferTargetGroup.text","queues.0.text"]),outputs:t=>Hb(fs(t).map(er),"__SUCCESS__")},y7={isTerminal:()=>!1,outputs:t=>{let e=t.path;if(!e||typeof e!="object")return[];let i=er(e,0);return i.outputId="__LOOP__",typeof e.label=="string"&&e.label.length>0||(i.label="Loop"),[i]}},P7={outputs:t=>Hb(fs(t).map(er),"__SUCCESS__")},j7={isTerminal:()=>!0,describe:t=>it(t,["flowName"]),outputs:t=>Hb(fs(t).map(er),"__DISCONNECT__")},S7={outputs:t=>{let e=fs(t);return e.length>0?[er(e[0],0)]:[]}},et=t=>({describe:t}),Yp={DisconnectAction:Sl,EndFlowAction:Sl,EndTaskAction:Sl,EndStateAction:Sl,ExitBotFlowAction:Sl,LoopAction:y7,ProcessVoicemailInputAction:P7,CallBotFlowAction:j7,AskForNLUNextIntentAction:S7,WaitForInputAction:{},CallTaskAction:{},TaskAction:{},TransferMenuAction:{},TransferTaskAction:{},LoopUntilAction:{},DecisionAction:et(wI),SwitchAction:et(wI),DataAction:et(t=>it(t,["actionName"])),DataTableLookupAction:et(t=>it(t,["datatableName"])),CallCommonModuleAction:et(t=>it(t,["flowName"])),CallBotConnectorAction:et(t=>it(t,["botName"])),CallDigitalBotFlowAction:et(t=>it(t,["flowName"])),AddFlowMilestoneAction:et(t=>it(t,["milestoneName"])),ScreenPopAction:et(t=>it(t,["inputs.0.value.text"])),PlayAudioAction:et(t=>it(t,["prompts.defaultAudio.text"])),UpdateVariableAction:{},SetAttributesAction:{},GetAttributesAction:{},EvaluateScheduleAction:{},CollectInputAction:{},GetResponseAction:{},AskForBooleanAction:{},AskForSlotAction:{},CommunicateAction:{},DigitalMenuAction:{},SendResponseAction:{},TranscriptionAction:{},FindQueueAction:{},FindUserPromptAction:{},SetWhisperAudio:{},BridgeServerAction:{}};function Xp(t){return typeof t=="string"&&t.length>0}function fs(t){let e=[];if(Array.isArray(t.paths))for(let i of t.paths)i&&typeof i=="object"&&e.push(i);return t.path&&typeof t.path=="object"&&e.push(t.path),e}function Ib(t){let e=t.replace(/^__/,"").replace(/__$/,"");if(e.length===0)return t;let i=e.toLowerCase();return i.charAt(0).toUpperCase()+i.slice(1)}function er(t,e){let i=t.outputId??`__OUTPUT_${e}__`,n=typeof t.label=="string"&&t.label.length>0?t.label:Ib(i),a={outputId:i,label:n,nextActionId:t.nextActionId,synthetic:!1,terminal:!1};return t.enabled===!1&&(a.disabled=!0),a}var O7=["taskReference","menuReference"],CI={describe:()=>{},outputs:t=>fs(t).map(er),isTerminal:()=>!1,referencedTaskIds:t=>{let e=[];for(let i of O7){let n=t[i];typeof n=="string"&&n.length>0&&e.push(n)}return e}},T7=new Set(["TransferMenuAction","TransferTaskAction"]);function AI(t){return typeof t=="string"&&t.startsWith("Transfer")&&!T7.has(t)}function bI(t){return t?t in Yp||AI(t):!1}function yI(t){let e=AI(t)?{...CI,...vI}:CI,i=t&&t in Yp?Yp[t]:void 0;return i?{...e,...i}:e}function*ed(t){for(let e of t.taskOrder){let i=t.tasks.get(e);if(i)for(let n of i.orderedActions)typeof n.id=="string"&&(yield{taskId:e,task:i,action:n,actionId:n.id,handler:yI(n.__type)})}}function PI(t){let e=[],i=[];for(let n of t.taskOrder){let a=t.tasks.get(n);if(a){Xp(a.startAction)&&e.push({from:vt(n),to:a.startAction,kind:"fallthrough"});for(let r of a.menuChoices)e.push({from:vt(n),to:r.actionId,kind:"reference",label:r.label})}}for(let{taskId:n,actionId:a,action:r,handler:s}of ed(t)){let o=s.isTerminal(r);Xp(r.nextAction)&&!o&&e.push({from:a,to:r.nextAction,kind:"fallthrough"});for(let l of s.outputs(r)){let u=ms(a,l.outputId);e.push({from:a,to:u,kind:"branch",label:l.label}),Xp(l.nextActionId)&&!l.terminal&&e.push({from:u,to:l.nextActionId,kind:l.outputId==="__LOOP__"?"loop":"branch"})}for(let l of s.referencedTaskIds(r)){let u=t.tasks.get(l);u?e.push({from:a,to:vt(l),kind:"reference",label:u.name}):i.push({code:"UNRESOLVED_REFERENCE",message:`Reference from ${a} to task ${l} is unresolvable.`,nodeId:a,taskId:n})}}return{edges:e,warnings:i}}function jI(t,e,i){let n=new Map;for(let u of t)n.set(u.id,u);let a=new Map;for(let u of n.keys())a.set(u,[]);let r=[],s=[],o=[];for(let u of e){let c=n.has(u.from),p=n.has(u.to);if(!c||!p){let d=c?`unknown target node "${u.to}"`:`unknown source node "${u.from}"`;s.push({...u,reason:d}),o.push({code:"DROPPED_EDGE",message:`Dropped ${u.kind} edge ${u.from} \u2192 ${u.to}: ${d}.`});continue}r.push(u),a.get(u.from)?.push(u.to)}let l=i.taskOrder.map(vt).filter(u=>n.has(u));return{graph:{nodes:n,edges:r,adjacency:a,roots:l,dropped:s},warnings:o}}function x7(t,e,i){let n=[],a=[];if(Array.isArray(t.actionList))for(let r of t.actionList)r&&typeof r=="object"&&n.push(r);if(Array.isArray(t.menuChoiceList))for(let r of t.menuChoiceList){let s=r?.action;if(!s||typeof s!="object")continue;if(typeof s.id!="string"){i.push({code:"MISSING_ACTION_ID",message:`Menu-choice action of type "${s.__type??"(missing)"}" in task ${e} has no id and was skipped.`,taskId:e});continue}n.push(s);let o=typeof r.name=="string"&&r.name.length>0?r.name:s.name??s.id;a.push({actionId:s.id,label:o})}return{orderedActions:n,menuChoices:a}}function M7(t){if(typeof t=="string")return t;if(t&&typeof t=="object"){let e=t.id;if(typeof e=="string")return e}}function SI(t){let e=[],i=new Map,n=Array.isArray(t.flowSequenceItemList)?t.flowSequenceItemList:[];for(let p of n){if(!p||typeof p!="object"||typeof p.id!="string")continue;let d=typeof p.name=="string"&&p.name.length>0?p.name:p.id,{orderedActions:h,menuChoices:g}=x7(p,p.id,e);i.set(p.id,{id:p.id,name:d,startAction:typeof p.startAction=="string"?p.startAction:void 0,orderedActions:h,menuChoices:g})}let a=new Set;for(let{id:p,orderedActions:d}of i.values())for(let h of d){if(typeof h.id!="string"){e.push({code:"MISSING_ACTION_ID",message:`Action of type "${h.__type??"(missing)"}" in task ${p} has no id and was skipped.`,taskId:p});continue}a.has(h.id)&&e.push({code:"DUPLICATE_ACTION_ID",message:`Duplicate action id "${h.id}" in task ${p}; the later definition wins.`,nodeId:h.id,taskId:p}),a.add(h.id)}let r=[...i.keys()],s=t.initialSequence,o=typeof s=="string"&&i.has(s),l=o?[s,...r.filter(p=>p!==s)]:r;typeof s=="string"&&s.length>0&&!o&&e.push({code:"UNRESOLVED_INITIAL_SEQUENCE",message:`initialSequence "${s}" matches no task; using declaration order.`});let u=new Set,c=t.uiMetaData?.task;if(Array.isArray(c))for(let p of c){let d=M7(p);d!==void 0&&u.add(d)}return{flowName:typeof t.name=="string"?t.name:"",flowType:typeof t.type=="string"?t.type:"",tasks:i,taskOrder:l,entryTaskId:o?s:void 0,reusableTaskIds:u,warnings:e}}var E7=new Set(["AskForNLUNextIntentAction","WaitForInputAction"]);function OI(t){let e=[],i=[];for(let n of t.taskOrder){let a=t.tasks.get(n);a&&e.push({id:vt(n),kind:"task-start",label:a.name,taskId:n,taskName:a.name,terminal:!1})}for(let{taskId:n,task:a,action:r,actionId:s,handler:o}of ed(t)){let l=r.__type,u=typeof r.name=="string"&&r.name.length>0?r.name:l??s;e.push({id:s,kind:"action",actionType:l,label:u,description:o.describe(r),taskId:n,taskName:a.name,terminal:o.isTerminal(r)}),bI(l)||i.push({code:"UNKNOWN_ACTION_TYPE",message:`Unknown action __type "${l??"(missing)"}" \u2014 handled generically.`,nodeId:s,taskId:n}),E7.has(l??"")&&i.push({code:"UNRESOLVED_INTENT_FANOUT",message:`Intent fan-out for ${s} is not resolved: per-intent routing (nluMetaData) is not modelled.`,nodeId:s,taskId:n});for(let c of o.outputs(r))e.push({id:ms(s,c.outputId),kind:"branch-output",label:c.label,taskId:n,taskName:a.name,terminal:c.terminal}),c.disabled&&i.push({code:"DISABLED_BRANCH",message:`Output ${c.outputId} of ${s} is disabled.`,nodeId:ms(s,c.outputId),taskId:n})}return{nodes:e,warnings:i}}function Rb(t){let e=new Map;for(let s of t.nodes.keys())e.set(s,0);let i=new Map,n=[],a=0;for(let s of t.roots){if(e.get(s)!==0)continue;e.set(s,1),i.set(s,a++);let o=[{node:s,index:0}];for(;o.length>0;){let l=o[o.length-1],u=t.adjacency.get(l.node)??[];if(l.indexp.code==="UNRESOLVED_INTENT_FANOUT");return{ok:!0,ir:fI(s,l,e,u),graph:s,warnings:[...e.warnings,...n,...r,...o]}}catch(e){return{ok:!1,error:{code:"INTERNAL",message:e instanceof Error?e.message:String(e)}}}}function k7(t,e){if(Array.isArray(t.actionList))for(let i of t.actionList)i&&typeof i=="object"&&e(i);if(Array.isArray(t.menuChoiceList))for(let i of t.menuChoiceList){if(!i||typeof i!="object")continue;let n=i.action;!n||typeof n!="object"||e(n,q7(i))}}function q7(t){let e={};return typeof t.id=="string"&&(e.id=t.id),typeof t.name=="string"&&(e.name=t.name),typeof t.digit=="string"&&(e.digit=t.digit),e}function _7(t){if(t===null||typeof t!="object"||Array.isArray(t))return[];let e=t.flowSequenceItemList;return Array.isArray(e)?e:[]}function Db(t,e){let i=[],n=new Set;for(let o of Array.isArray(e)?e:[])typeof o=="string"&&!n.has(o)&&(n.add(o),i.push(o));if(i.length===0)return{found:[],notFound:[]};let a=new Map;for(let o of _7(t)){if(!o||typeof o!="object"||typeof o.id!="string")continue;let l=o.id,u=typeof o.name=="string"&&o.name.length>0?o.name:l;k7(o,(c,p)=>{if(typeof c.id!="string"||!n.has(c.id))return;let d={actionId:c.id,action:c,taskId:l,taskName:u};p&&(d.menuChoice=p);let h=a.get(c.id);h?h.push(d):a.set(c.id,[d])})}let r=[],s=[];for(let o of i){let l=a.get(o);l?r.push(...l):s.push(o)}return{found:r,notFound:s}}var TI=25;function H7(t){let e=[],i=new Map,n=!1;for(let a of t){let r=a.split("::")[0]||a;r!==a&&(n=!0),i.has(r)||(i.set(r,a),e.push(r))}return{lookupIds:e,requestedFor:i,strippedAny:n}}var I7={flowId:We.string().min(1).describe("The Genesys Cloud Architect flow ID"),actionIds:We.array(We.string().min(1)).min(1).max(TI).describe(`Action GUIDs, taken from the \`id\` of flow_ir nodes whose \`kind\` is "action". Batch every action of interest into one call rather than calling this tool repeatedly. Branch-output ids of the form \`::\` are also accepted; the suffix is stripped and the underlying action is returned. Maximum ${TI} ids.`)},xI=({architectApi:t})=>({config:{description:"Retrieves the raw Architect configuration of specific actions in a Genesys Cloud Architect flow: decision expressions, prompt text, data action inputs and outputs, and any other per-action settings that the flow_ir tool omits. Use flow_ir first to find action ids and to understand the control flow, then use this tool to find out what an action actually does. Do not use the raw fields returned here to make control-flow claims: flow_ir has already resolved those, and its answer accounts for branches the raw configuration does not reflect.",annotations:{title:"Flow Action",readOnlyHint:!0,destructiveHint:!1},inputSchema:I7},handler:async({flowId:e,actionIds:i})=>{let n;try{n=await t.getFlowLatestconfiguration(e)}catch{return{isError:!0,content:[{type:"text",text:`Flow "${e}" not found or not accessible.`}]}}let{lookupIds:a,requestedFor:r,strippedAny:s}=H7(i),o=Db(n,a),l=c=>({...c,actionId:r.get(c.actionId)??c.actionId}),u=[];return s&&u.push("Some requested ids were flow_ir synthetic node ids of the form ::; the suffix was stripped and the underlying action looked up, then reported under the id as requested. A ::start id identifies a task rather than an action, so it never matches."),o.notFound.length>0&&u.push("Ids in notFound are absent from this flow's latest configuration. The flow may have been redeployed since flow_ir was called, so re-run flow_ir and re-join rather than concluding those actions no longer exist."),{content:[{type:"text",text:JSON.stringify({flowId:e,found:o.found.map(l),notFound:o.notFound.map(c=>r.get(c)??c),...u.length>0?{notes:u}:{}})}]}}});function MI(t){let e=t.toUpperCase();return e.endsWith("FLOW")?e:`${e}FLOW`}function R7(t,e){let i={};for(let n of e){let a=n.type??"UNKNOWN";i[a]||(i[a]=[]),i[a].push({id:n.id??"",name:n.name??"",...n.version?{version:n.version}:{},deleted:n.deleted??!1,updated:n.updated??!1})}return{flow:{id:t.id??"",name:t.name,type:MI(t.type??""),version:t.publishedVersion?.commitVersion??"1"},dependencies:i}}var z7={flowId:We.string().min(1).describe("The Genesys Cloud Architect flow ID")},EI=({architectApi:t})=>({config:{description:"Retrieves all dependencies consumed by a Genesys Cloud Architect flow. Returns the flow metadata and its dependencies grouped by type.",annotations:{title:"Flow Dependencies",readOnlyHint:!0,destructiveHint:!1},inputSchema:z7},handler:async({flowId:e})=>{try{let i;try{i=await t.getFlow(e)}catch{return{isError:!0,content:[{type:"text",text:`Flow "${e}" not found.`}]}}let n=MI(i.type??""),a=i.publishedVersion?.commitVersion??"1",r=[],s=1;for(;;){let l=await t.getArchitectDependencytrackingConsumedresources(i.id,a,n,{pageSize:100,pageNumber:s});if(l.entities&&r.push(...l.entities),!l.nextUri)break;s++}let o=R7(i,r);return{content:[{type:"text",text:JSON.stringify(o,null,2)}]}}catch(i){return{isError:!0,content:[{type:"text",text:`Failed to retrieve flow dependencies: ${i instanceof Error?i.message:String(i)}`}]}}}});function D7(t,e){let i=t.find(r=>r.id===e);if(i)return{match:i};let n=e.toLowerCase(),a=t.filter(r=>r.name.toLowerCase()===n);return a.length>1?{ambiguous:a}:a[0]?{match:a[0]}:void 0}var G7={flowId:We.string().min(1).describe("The Genesys Cloud Architect flow ID"),task:We.string().min(1).optional().describe("Optional. Restrict the returned nodes to a single task, by task id or task name (case-insensitive). Use this to explore a large flow one task at a time. The full task list is always returned, and a node's predecessors and successors may reference nodes in other tasks, which will not appear in the filtered node list.")},kI=({architectApi:t})=>({config:{description:"Retrieves the intermediate representation (IR) of a Genesys Cloud Architect flow: a flat, ordered list of its actions with the branches connecting them. Use this to understand what an existing flow does. It answers what follows an action, which branch leads where, which actions are unreachable, and how tasks call each other.",annotations:{title:"Flow IR",readOnlyHint:!0,destructiveHint:!1},inputSchema:G7},handler:async({flowId:e,task:i})=>{let n;try{n=await t.getFlowLatestconfiguration(e)}catch{return{isError:!0,content:[{type:"text",text:`Flow "${e}" not found or not accessible.`}]}}let a=zb(n);if(!a.ok)return{isError:!0,content:[{type:"text",text:`Failed to parse flow "${e}" (${a.error.code}): ${a.error.message}`}]};let r=a.ir;if(typeof i=="string"){let s=D7(r.tasks,i);if(!s){let o=r.tasks.map(l=>l.name).join(", ");return{isError:!0,content:[{type:"text",text:`No task matching "${i}" in flow "${e}". Available tasks: ${o||"(none)"}.`}]}}if("ambiguous"in s){let o=s.ambiguous.map(l=>`"${l.name}" (id: ${l.id})`).join(", ");return{isError:!0,content:[{type:"text",text:`Task name "${i}" is ambiguous in flow "${e}". It matches ${o}. Retry with the task id.`}]}}r={...r,nodes:r.nodes.filter(o=>o.taskId===s.match.id)}}return{content:[{type:"text",text:JSON.stringify({flowId:e,ir:r,warnings:a.warnings})}]}}});var Gb=new Map;function $7(t){let{id:e,previousTurn:i,...n}=t;return n}async function qI(t,e,i){let n=[],a=i;for(;n.push($7(a)),Gb.set(e,a.id),a.nextActionType==="NoOp";)a=await t.postTextbotsBotflowsSessionTurns(e,{previousTurn:{id:a.id},inputEventType:"NoOp",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:""}}]}});let r=a.nextActionType;return(r==="Disconnect"||r==="Exit")&&Gb.delete(e),{content:[{type:"text",text:JSON.stringify({sessionId:e,turns:n},null,2)}]}}var N7={flowId:We.string().optional().describe("The bot flow ID to test. Required when starting a new session."),sessionId:We.string().optional().describe("Session ID from a previous call. Required when continuing an existing conversation."),message:We.string().optional().describe("User message to send to the bot. Required when continuing a session.")},_I=({textbotsApi:t})=>({config:{description:"Tests a deployed Genesys Cloud Architect Bot Flow and Digital Bot Flow by simulating a text conversation. To start: provide flowId. To continue: provide the returned sessionId and a message. Turn tracking is managed server-side \u2014 only the sessionId is needed between calls. Returns { sessionId, turns[] } where each turn contains the raw API response (prompts with segments, outputLanguage, nextActionType, and action-specific data like modeConstraints or outputData). Multiple turns are returned when the flow sends consecutive messages (NoOp turns are automatically drained). nextActionType values: WaitForInput (send another message), Disconnect/Exit (conversation ended).",annotations:{title:"Test Bot Flow",readOnlyHint:!1,destructiveHint:!1},inputSchema:N7},handler:async({flowId:e,sessionId:i,message:n})=>{try{if(i&&e)return{isError:!0,content:[{type:"text",text:"Provide either flowId (to start) or sessionId (to continue), not both."}]};if(!i&&!e)return{isError:!0,content:[{type:"text",text:"Provide flowId to start a new session or sessionId to continue an existing one."}]};if(e){let s=await t.postTextbotsBotflowsSessions({flow:{id:e},externalSessionId:"",inputData:{variables:{}},channel:{inputModes:["Text"],outputModes:["Text"],name:"Messaging",userAgent:{name:"GenesysWebWidget"}},language:""}),o=await t.postTextbotsBotflowsSessionTurns(s.id,{inputEventType:"NoOp",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:""}}]}});return qI(t,s.id,o)}if(!i||!n)return{isError:!0,content:[{type:"text",text:"A message is required when continuing an existing session."}]};let a=Gb.get(i);if(!a)return{isError:!0,content:[{type:"text",text:`No active session found for ID "${i}". It may have expired or already ended. Start a new session with a flowId.`}]};let r=await t.postTextbotsBotflowsSessionTurns(i,{previousTurn:{id:a},inputEventType:"UserInput",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:n}}]}});return qI(t,i,r)}catch(a){return{isError:!0,content:[{type:"text",text:`Bot flow test failed: ${a instanceof Error?a.message:JSON.stringify(a)}`}]}}}});var $b=We.object({GENESYS_REGION:We.string().min(1),GENESYS_CLIENT_ID:We.string().min(1),GENESYS_CLIENT_SECRET:We.string().min(1),DEPLOY_SCRIPT_PATH:We.string().min(1),PREVENT_LOGIN:We.enum(["TRUE","FALSE"]).default("FALSE").transform(t=>t==="TRUE")}).safeParse(process.env);if(!$b.success){let t=$b.error.issues.map(e=>e.path[0]).join(` `);console.error(`Missing required environment variables: ${t}`),process.exit(1)}var Qt=$b.data,ws=new mc({name:"genesys-cloud-architect",version:"1.0.6"}),Nb=new id.default.ArchitectApi,HI=EI({architectApi:Nb});ws.registerTool("flow_dependencies",HI.config,HI.handler);var II=kI({architectApi:Nb});ws.registerTool("flow_ir",II.config,II.handler);var RI=xI({architectApi:Nb});ws.registerTool("flow_action",RI.config,RI.handler);var zI=mI({region:Qt.GENESYS_REGION,clientId:Qt.GENESYS_CLIENT_ID,clientSecret:Qt.GENESYS_CLIENT_SECRET,deployScriptPath:Qt.DEPLOY_SCRIPT_PATH});ws.registerTool("deploy_flow",zI.config,zI.handler);var DI=_I({textbotsApi:new id.default.TextbotsApi});ws.registerTool("test_bot_flow",DI.config,DI.handler);(async()=>{if(Qt.PREVENT_LOGIN)console.warn("Login for Platform API skipped. Calling tools will result in an auth failure.");else{let e=id.default.ApiClient.instance;e.setEnvironment(Qt.GENESYS_REGION),await e.loginClientCredentialsGrant(Qt.GENESYS_CLIENT_ID,Qt.GENESYS_CLIENT_SECRET)}let t=new wc;await ws.connect(t)})().catch(t=>{console.error("Failed to start server:",t),process.exit(1)}); /*! Bundled license information: diff --git a/skills/interpret-flow-ir/SKILL.md b/skills/interpret-flow-ir/SKILL.md index baf5b47..86275e8 100644 --- a/skills/interpret-flow-ir/SKILL.md +++ b/skills/interpret-flow-ir/SKILL.md @@ -42,7 +42,8 @@ Each node: | `actionType` | Architect `__type` (e.g. `DecisionAction`), actions only | | `label` | Human-readable name (action name, task name, or branch label like `Failure`) | | `description` | Optional summary of what the action does | -| `predecessors` | **Incoming** edges `{ id, label?, backEdge }`; see "Navigating" | +| `predecessors` | **Incoming** edges `{ id, label?, backEdge }`; `id` is the source node. See "Navigating" | +| `successors` | **Outgoing** edges `{ id, label?, backEdge }`; `id` is the target node. Mirror of `predecessors`; see "Navigating" | | `order` | DFS discovery order, not execution order; sibling branches appear sequentially | | `taskId`, `taskName` | Owning task | | `reachable` | Reached by DFS from **any** task-start; see the orphaned-tasks recipe | @@ -57,30 +58,36 @@ Each node: actions in a task". - **`branch-output`**: one per outcome of a branching action (a Decision's Yes/No, a data action's Success/Failure, a loop's body). Not an action; it is - a labelled fork. A branch-output with no successors is a dangling outcome + a labelled fork. A branch-output with `successors: []` is a dangling outcome (see recipes). ## Navigating the graph -Edges are stored **incoming**: each node lists its predecessors, not its -successors. To answer "what happens next", invert once: +Every node carries its edges **both ways**. Pick the direction that matches the +question instead of deriving one from the other: -1. Build a successor map: for every node N and every predecessor P in - `N.predecessors`, record "P leads to N" with the predecessor's `label` and - `backEdge`. -2. Trace forward from `::start` for the caller-visible path. Pass - through branch-output nodes, using their `label` as the branch condition - ("on Failure, ..."). If `entryTaskId` is absent, say the entry is unknown - rather than guessing a starting task. -3. Stop a trace at `terminal: true` nodes. +- **`successors`** (outgoing) for forward walks — "the caller presses 1, then + what?", "what happens after this action". +- **`predecessors`** (incoming) for backwards walks — "what leads here", "what + condition guards this action", "does anything call this task". -Edge `label` carries the branch or jump meaning: branch outcome labels -(`Success`, `Failure`, `Yes`/`No`), IVR menu choice names, and the target task -name on jump edges. `backEdge: true` marks a real cycle. +There is never a reason to build a successor map by inverting `predecessors`; +the IR already ships both lists, mirrored and deduped. -An unlabelled predecessor pointing directly at a branching action (not at one -of its branch-outputs) is that action's fall-through: the path taken after the -action completes, e.g. a loop's continue-after-exit edge. +**Forward trace.** Start at `::start` and follow `successors`, +passing through branch-output nodes and using their `label` as the branch +condition ("on Failure, ..."). If `entryTaskId` is absent, say the entry is +unknown rather than guessing a starting task. Stop a trace at `terminal: true` +nodes. + +Edge `label` carries the branch or jump meaning in **both** lists: branch +outcome labels (`Success`, `Failure`, `Yes`/`No`), IVR menu choice names, and +the target task name on jump edges. `backEdge: true`, in either list, marks a +real cycle. + +An unlabelled **successor** leaving a branching action directly (rather than +leaving one of its branch-outputs) is that action's fall-through: the path taken +after the action completes, e.g. a loop's continue-after-exit edge. Never narrate `nodes` in array order as if it were the call sequence. `order` is depth-first discovery: after a branch, one entire arm appears before the @@ -95,9 +102,9 @@ optional `task` parameter to fetch one task at a time: shared by several tasks is refused with the candidate ids; retry with an id. - `ir.tasks` always lists every task even when filtered, so the full inventory survives; walk tasks one call each. -- A filtered node's `predecessors` may name ids from other tasks; those ids are - absent from the filtered `nodes` array. That is a cross-task jump, not a - dangling reference. +- A filtered node's `predecessors` and `successors` may both name ids from other + tasks; those ids are absent from the filtered `nodes` array. That is a + cross-task jump, not a dangling reference. ## Action semantics: the `flow_action` tool @@ -169,9 +176,10 @@ narrating action labels and branch labels at each fork. Present paths as the caller would experience them, not as node ids. **Missing error handling**: find `branch-output` nodes whose label indicates -failure, error, or timeout, with `terminal: false` and no successors. That -outcome silently drops out of the flow. (A terminal branch-output with no -successor is correct, e.g. a transfer's Success leaves the flow by design.) +failure, error, or timeout, with `terminal: false` and `successors: []`. That +outcome silently drops out of the flow. (A terminal branch-output with +`successors: []` is correct, e.g. a transfer's Success leaves the flow by +design.) **Dead logic**: first check `reachabilityIsComplete`. When `true`, `reachable: false` nodes (grouped by `taskName`) are provably orphaned actions @@ -186,8 +194,9 @@ task's `::start` node (excluding `entryTaskId`): **zero predecessors means nothing jumps to it**. Qualify this too when `reachabilityIsComplete` is false, since an intent may jump to the task. -**Loops**: any predecessor with `backEdge: true` closes a real cycle. Describe -the cycle path and check it has a terminal or branch exit. +**Loops**: any predecessor with `backEdge: true` closes a real cycle; walking +forward, the same cycle shows as a successor with `backEdge: true`. Describe the +cycle path and check it has a terminal or branch exit. **How does the flow end**: list `terminal: true` action nodes (disconnects, end-flow/end-task, transfers). Transfers are terminal on success only; their diff --git a/src/mcp-server/tools/flow-ir.ts b/src/mcp-server/tools/flow-ir.ts index 8554e2c..06cdeaf 100644 --- a/src/mcp-server/tools/flow-ir.ts +++ b/src/mcp-server/tools/flow-ir.ts @@ -41,8 +41,8 @@ const inputSchema = { "Optional. Restrict the returned nodes to a single task, by task id or " + "task name (case-insensitive). Use this to explore a large flow one " + "task at a time. The full task list is always returned, and a node's " + - "predecessors may reference nodes in other tasks, which will not " + - "appear in the filtered node list.", + "predecessors and successors may reference nodes in other tasks, which " + + "will not appear in the filtered node list.", ), };