Booking Agent
We will implement a Hotel Booking supervisor based agent with a chat UI and a Human in The Loop Approval step. This is the supervisor agent shipped with the templates. Crucially, the Human In the Loop approval is taken by the sub-agent.
You can find the github repo here : https://github.com/cascaide-ts/cascaide-nextjs-starter.git Some tool definitions, prompts, etc have been skipped in this guide for brevity sake. Please refer to the full code in the repo.
Problem Statement: A supervisor agent based system that searches for hotel bookings, displays it to the user and books a hotel with payment approval.
This will be a long tutorial. We will build a three-agent supervisor system. A supervisor, an availability agent, and a booking agent — with a chat UI, inline hotel selection, and a payment approval overlay triggered by the sub-agent itself. Deployment ready. Multi-tenant by default.
Prerequisites:
- Set up an application following the quickstart guide.
We assume that you are using NextJS here, but the same principles apply to any stack.
Flow of Implementation
- Step 1: Identify the nodes required
- Step 2: Define the nodes
- Step 3: Set up the graphs and configurations
- Step 4: Provide the configurations to workflowHandler and WorkflowProvider
Identify the Nodes Required
We need three agents in all.
- Supervisor agent that delegates to subagents, displays available hotels and accepts booking intent
- Availability agent, that searches for available hotels and provides the information to the supervisor
- Booking agent that takes HITL approval, books the hotel and confirms the booking with the supervisor
Let’s start with our UI node requirements
- Chat UI node: This will be the first node and the entry point. It will always be active. | Client Node
- Booking UI Node: This node will take human approval and mock an API call to process payment, and will vanish once done. | Client Node
Now, let us identify the nodes required for the agents
supervisorAgentNodeto make the llm call with the supervisor prompt.- We skip the toolNode for the supervisor as it only delegates or presents hotel options (this is a different way of doing HITL, more information below)
availabilityAgentNodeto make the llm call with the availability agent promptavailabilityToolNodeto execute the hotel searchbookingAgentNodeto make the llm call with booking agent prompt- We skip the tool node for booking agent because it only has the booking ui node as the tool.
Note : There is actually a second separate method for HITL that you can use if
- you need the UI to stay inside the chat even after human input (this is what most other AI chat libraries do)
- you only need to display some data in a UI (like a chart for example)
We will discuss it in more detail.
Defining Nodes
supervisorAgentNode
prep and exec follows the same logic as a standard ReAct agent. post is what needs to be explained.
type hotelSupervisorPrepOut = {
history: any[]; //Todo : type should be providerMessage[], as we convert before passing
cascadeId: string;
};
export const hotelSupervisorNode: StreamingServerNodeDefinition<hotelSupervisorPrepOut> = {
name: 'hotelSupervisorNode',
isUINode: false,
env: 'server',
isStreaming: true,
async prep(cascadeContext: WorkflowContext, initialContext: any): Promise<hotelSupervisorPrepOut> {
const cascadeId = initialContext.cascadeId;
const dataArray = cascadeContext[cascadeId];
const canonicalHistory = dataArray.flatMap((item: any) => item.history || []);
const history = toProviderHistory('gemini-genai', canonicalHistory);
return { history, cascadeId };
},
async exec(prepOutput: hotelSupervisorPrepOut, controller?: CascadeController): Promise<StreamConfig> {
const { history } = prepOutput;
console.log(JSON.stringify(history));
const systemPrompt = `
### ROLE
You are a Hotel Booking Supervisor. Your job is to act as the central brain, routing user requests to the correct sub-agent and maintaining the state of the booking process.
### SUB-AGENTS
1. Availability_Checker: Use this when the user asks about specific hotels or general availability.
2. Payment_Processor: Use this after 'present_hotel_options' tool gives you the date and, hotel and room type to book
### OPERATIONAL RULES
- If the user intent is vague, call Availability_Checker.
- If details are missing for booking, prompt the user.
- Use present_hotel_options to display the available hotels.
**CRITICAL**: Always use present_hotel_options tool to present results.
`.trim();
/*
We will construct the tools here. We will write the tool definitions in `ToolParam` shape and
convert it into provider specific shape using `buildTools` helper.
*/
const hotelToolParams: ToolParam[] = [
{
name: 'delegate_to_availabilityAgent',
description:
'Delegates availability-related tasks. Use this for checking slots, finding open turfs, or answering questions about slot timings of the turfs.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description:
'The natural language instruction for the sub-agent. Example: "Check if Apex Arena has slots open after 6 PM today."',
},
},
required: ['query'],
},
},
{
name: 'delegate_to_bookingAgent',
description:
'Delegates payment and booking finalization. Use this when the user is ready to pay for a specific turf and slot.',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description:
'The natural language instruction for the payment agent. Example: "Process payment for Downtown Turf for the 7 PM - 8 PM slot."',
},
},
required: ['query'],
},
},
{
name: 'present_hotel_options',
description:
'Presents a list of hotels with their main image, description, and available room types with pricing.',
parameters: {
type: 'object',
properties: {
hotels: {
type: 'array',
description: 'List of hotels to present to the user.',
items: {
type: 'object',
properties: {
hotel_name: { type: 'string' },
hotel_image_url: { type: 'string' },
description: { type: 'string' },
available_rooms: {
type: 'array',
items: {
type: 'object',
properties: {
room_type: { type: 'string' },
price: { type: 'number' },
},
required: ['room_type', 'price'],
},
},
},
required: ['hotel_name', 'hotel_image_url', 'description', 'available_rooms'],
},
},
},
required: ['hotels'],
},
} as unknown as ToolParam,
];
const geminiTools = buildTools('gemini-genai', hotelToolParams);
const {stream, provider} = await callLLM('gemini-genai', 'gemini-3-flash-preview', systemPrompt, history, geminiTools, true);
return { stream, provider };
},
async post(execOutput: StreamingExecOutput): Promise<PostResult> {
const { assistantMessage, uiAssistantMessage, history, cascadeId, userId } = execOutput;
const pendingToolCalls = assistantMessage.tool_calls ?? [] as CanonicalToolCall[]
console.log(JSON.stringify(assistantMessage));
const isDifferent = uiAssistantMessage !== undefined &&
JSON.stringify(assistantMessage) !== JSON.stringify(uiAssistantMessage);
return {
updates: {
[cascadeId as string]: {
history: [assistantMessage as CanonicalMessage],
status: pendingToolCalls.length > 0 ? 'calling_tool' : 'complete',
lastUpdate: Date.now(),
//you can add more fields if you want, it will be written to the cascade state
// what's mandatory is history and status
},
} as Updates,
...(isDifferent ? {
uiUpdates: {
[cascadeId as string]: { history: [uiAssistantMessage as CanonicalMessage], status: pendingToolCalls.length > 0 ? 'calling_tool' : 'complete' },
},
} : {}),
spawns: undefined,
};
}
};prep
This is identical to any ReAct agent. We flatmap out the history from the state.
exec
We provide 3 tools to the supervisor. 2 delegation tools and one to present hotel options.
Since it is a streaming node, we return StreamConfig.
post
return {
updates: {
[cascadeId as string]: {
history: [assistantMessage as CanonicalMessage],
status: pendingToolCalls.length > 0 ? 'calling_tool' : 'complete',
lastUpdate: Date.now(),
//you can add more fields if you want, it will be written to the cascade state
// what's mandatory is history and status
},
} as Updates,
...(isDifferent ? {
uiUpdates: {
[cascadeId as string]: { history: [uiAssistantMessage as CanonicalMessage], status: pendingToolCalls.length > 0 ? 'calling_tool' : 'complete' },
},
} : {}),
spawns: undefined,
};Notice we don’t spawn anything. This is because
-
delegation tools are handled client side. We essentially terminate the cascade so that delegations can be handled client side, and then restart the supervisor cascade with the results of the subagents. We will discuss this in more detail in the
Chatsection. It is also discussed in the Prebuilt Chat UI page of the UI/UX section. -
the “better” way to handle delegation is by using the
controller. However, we are using this alternate method of using the client to coordinate for the purposes of this tutorial. Please look at recursive ReAct agent tutorial for a concrete example on how delegation can be handled entirely server side. -
present_hotel_options is an HITL tool. When the state is updated with the
assistantMessage, this tool call is available to theChatUI. We can render it inline and then restart the cascade with the users’s selection as a tool response. -
In both these cases, from the supervisor agent’s perspective, it made a tool call and got a response back.
As discussed above we skip the supervisor tool node because we don’t need it.
availabilityAgentNode
This works exactly as a ReAct agent. Code provided for clarity.
prep
export async function availabilityAgentNodePrep(context: WorkflowContext, initialContext: any) {
const cascadeId = initialContext.cascadeId;
const dataArray = context[cascadeId]
const history = dataArray.flatMap(item => item.history || []);
return { history };
}exec
export async function availabilityAgentNodeExec(prepOutput: any) {
const { history} = prepOutput;
const OpenAI = (await import('openai')).default;
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const available_hotels = {
type: 'function' as const,
function: {
name: 'available_hotels',
description: 'Fetches a JSON List of hotels,rooms their prices nad other additional details',
parameters: {
type: 'object',
properties: {},
required: [],
},
},
};
try {
const systemPrompt =`
### ROLE
You are the hotel retrieval Agent. Your sole purpose is to provide accurate information about available hotels and their prices.
### GOALS
1. When you receive a query, ALWAYS call the 'available_hotels' tool first to get the latest data.
2. Filter the results based on the user's specific request (e.g., if they ask for a specific suite or a price constraint).
3. If the user's request is general (e.g., "What is available?"), summarize the top options from the available data.
4. Always use return the image_urls of the hotels fetched along with the data
### RESPONSE GUIDELINES
- Be concise.
- If a user asks for a hotel that doesn't exist in the data, politely inform them of the available hotels.
- Format your output so the Supervisor Agent can easily present it to the end user.
`
.trim();
const conversationHistory = [
{ role: 'system' as const, content: systemPrompt },
...history,
];
const stream = await openai.chat.completions.create({
model: 'gpt-4.1-mini-2025-04-14',
stream: true,
messages: conversationHistory as any,
tools: [available_hotels],
tool_choice: 'auto',
temperature: 1,
});
return { stream, provider: 'openai',isReasoning: false} as StreamConfig;
} catch (error: any) {
throw new Error(`Agent execution failed: ${error.message}`);
}
}
post
export async function availabilityAgentNodePost(execOutput: any) {
const { assistantMessage, cascadeId } = execOutput;
const pendingToolCalls = assistantMessage.tool_calls || [];
const shouldSpawnNode = pendingToolCalls.length > 0 ;
const updates = {
[cascadeId]: {
history: assistantMessage,
status: pendingToolCalls.length > 0 ? 'calling_tool' : 'complete',
},
};
const spawns: Record<string, any> = {};
if (shouldSpawnNode) {
spawns['availabilityToolNode'] = {
history: assistantMessage,
toolCallsToExecute: pendingToolCalls,
};
}
return {
updates,
spawns: Object.keys(spawns).length > 0 ? spawns : undefined,
};
}availabilityToolNode
A standard tool node like in the other examples. Executes any tool calls in parallel.
prep
export async function availabilityToolNodePrep(selectedData: any, initialContext: any) {
const toolCallsToExecute = initialContext.toolCallsToExecute || [];
if (toolCallsToExecute.length === 0) {
if (!initialContext.hasOwnProperty('toolCallsToExecute')) {
throw new Error('toolNodePrep Error: No specific toolCallsToExecute array was provided in the context.');
}
}
return { toolCallsToExecute, cascadeId: initialContext.cascadeId};
}exec
export async function availabilityToolNodeExec(prepOutput: any) {
'use server';
const { toolCallsToExecute, cascadeId} = prepOutput;
const { executeTool } = await import('./availabilityTools');
const results = await Promise.all(
toolCallsToExecute.map(async (toolCall: ToolCall) => {
try {
const toolResult = await executeTool(toolCall);
return { toolCall, toolResult };
} catch (err: any) {
return {
toolCall,
toolResult: {
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({ error: err.message || 'Tool execution failed' }),
},
};
}
})
);
return { results, cascadeId};
}
export type ToolCall = {
id: string;
type: 'function';
function: {
name: string;
arguments: string;
};
};
export type ToolResult = {
role: 'tool';
tool_call_id: string;
content: string;
};
function getHardcodedHotelAvailability(): string {
const hostelData = [
{
id: "t1",
hotel: "Sunview Beach Resort",
available_rooms: [{'room type':'deluxe suite','perNightCost':4000},{'room type':'double bed','perNightCost':2000}],
description:"Close to Papanasam Beach, this opulent property features comfortable rooms, an azure swimming pool, a private beach and a host of modern amenities.There is a sauna room available to cater to your wellness needs.",
image_url:"https://gos3.ibcdn.com/13f68fb5-1551-4b5b-9cff-64296e2f85e0.jpeg"
},
{
id: "t2",
hotel: "Elixir Cliff Beach Resort",
available_rooms: [{'room type':'deluxe seaview suite','perNightCost':6000}],
description:"Overlooking the majestic Arabian Sea, this lavish property features stunning rooms, an incredible dining spot, an infinity pool and an extensive range of facilities.",
image_url:"https://gos3.ibcdn.com/f20a131c53ed11eb90830242ac110002.jpg"
},
{
id: "t3",
hotel: "WEST BAY MARISOL",
available_rooms: [{'room type':'standard','perNightCost':4000},{'room type':'deluxe','perNightCost':6000},{'room type':'executive','perNightCost':9000}],
description:"The property offers a welcoming and comfortable environment, featuring a range of well-appointed rooms designed for both relaxation and convenience. With modern amenities, exceptional service, and a prime location, it caters to both business and leisure travelers. Guests can enjoy various on-site facilities, including dining options, recreational areas, and more, ensuring a memorable stay.",
image_url:"https://gos3.ibcdn.com/2a3a165f-ec99-4644-97e2-af49828123e3.jpg"
},
{
id: "t4",
hotel: "Zostel Varkala",
available_rooms: [{'room type':'mixed dorm','perNightCost':1000},{'room type':'standard','perNightCost':2000},{'room type':'A-Frame Cottage','perNightCost':5000}],
description:"Set amid swaying coconut palms and facing the Arabian Sea, this scenic property is a 5-minute stroll from Varkala’s famed Black Sand Beach.The rooftop is equipped with patio loungers and inviting spots to work, dine, paint, or simply soak in the panoramic sea vistas.",
image_url:"https://dynamic-media-cdn.tripadvisor.com/media/photo-o/1a/23/20/5a/zostel-varkala-terrace.jpg?w=900&h=500&s=1"
},
{
id: "t5",
hotel: "Eva Beach Hotel",
available_rooms: [{'room type':'DELUXE AC','perNightCost':4000},{'room type':'standard AC','perNightCost':3000}],
description:"Located in Varkala Cliff, within 100 meters of Varkala Beach and 600 meters of Odayam Beach, Eva Beach Hotel provides accommodation with a terrace, free wifi throughout the property, and free private parking for guests who drive. Rooms are complete with a private bathroom, while certain rooms at the resort also offer a seating area.Popular points of interest near Eva Beach Hotel include Aaliyirakkm Beach, Varkala Cliff, and Janardhanaswamy Temple. The nearest airport is Thiruvananthapuram International, 41 km from the accommodation, and the property offers a paid airport shuttle service.",
image_url:"https://gos3.ibcdn.com/0f02787c-de9e-493e-bf7f-ab0d50b8c17e.jpeg"
}
];
return JSON.stringify(hostelData, null, 2);
}
export const executeTool = async (toolCall: ToolCall): Promise<ToolResult> => {
const { name, arguments: argsString } = toolCall.function;
try {
const args = JSON.parse(argsString);
let toolResultData: any;
switch (name) {
case 'available_hotels':
toolResultData = getHardcodedHotelAvailability();
break;
default:
toolResultData = { error: `Unknown tool: ${name}` };
break;
}
return {
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(toolResultData),
};
} catch (error: any) {
return {
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({
error: `Tool execution failed: ${error.message}`,
}),
};
}
};post
Writes tool results into the state and spawns availabilityAgentNode.
export async function availabilityToolNodePost(execOutput: any) {
const { results, cascadeId } = execOutput;
const toolResultsOnly = results.map((r: any) => r.toolResult);
return {
updates: {
[cascadeId]: {
history: toolResultsOnly,
status: 'complete',
},
},
spawns: {
'availabilityAgentNode': {
history: toolResultsOnly,
},
},
};
}bookingAgentNode
prep
export async function bookingAgentNodePrep(context: WorkflowContext, initialContext: any) {
const cascadeId = initialContext.cascadeId;
const dataArray = context[cascadeId]
const history = dataArray.flatMap(item => item.history || []);
return { history, cascadeId};
}exec
export async function bookingAgentNodeExec(prepOutput: any) {
const { history, cascadeId } = prepOutput;
const OpenAI = (await import('openai')).default;
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
try {
const conversationHistory = [
{ role: 'system' as const, content: systemPrompt },
...history,
];
const stream = await openai.chat.completions.create({
model: 'gpt-4.1-mini-2025-04-14',
stream: true,
messages: conversationHistory as any,
tools: [
processBookingPayment
],
tool_choice: 'auto',
temperature: 0.7,
});
const assistantMessage: Message = { role: 'assistant', content: '', tool_calls: [] };
for await (const chunk of stream) {
const delta = chunk.choices?.[0]?.delta;
if (!delta) continue;
if (delta.content) {
assistantMessage.content = (assistantMessage.content || '') + delta.content;
}
if (delta.tool_calls) {
for (const toolCall of delta.tool_calls) {
const index = toolCall.index;
if (!assistantMessage.tool_calls![index]) {
assistantMessage.tool_calls![index] = { id: '', type: 'function', function: { name: '', arguments: '' } };
}
if (toolCall.id) assistantMessage.tool_calls![index].id = toolCall.id;
if (toolCall.function?.name) assistantMessage.tool_calls![index].function.name = toolCall.function.name;
if (toolCall.function?.arguments) assistantMessage.tool_calls![index].function.arguments += toolCall.function.arguments;
}
}
}
if (!assistantMessage.content) assistantMessage.content = null;
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) delete assistantMessage.tool_calls;
return { assistantMessage, cascadeId};
} catch (error: any) {
throw new Error(`Agent execution failed: ${error.message}`);
}
}This agent assembles the stream into assistantMessage inside exec. Strictly speaking, unnecessary.
post
export async function bookingAgentNodePost(execOutput: any) {
const { assistantMessage, cascadeId} = execOutput;
const toolCallsToExecute = assistantMessage.tool_calls || [];
const shouldSpawnNode = toolCallsToExecute.length > 0 ;
const updates = {
[cascadeId]: {
history: assistantMessage,
status: toolCallsToExecute.length > 0 ? 'calling_tool' : 'complete',
},
};
const spawns: Record<string, any> = {};
if (shouldSpawnNode) {
spawns['bookingUiNode'] = {
history: assistantMessage,
toolCallsToExecute: toolCallsToExecute,
};
}
return {
updates,
spawns: Object.keys(spawns).length > 0 ? spawns : undefined,
};
}Notice how we are directly spawning bookingUiNode. As discussed above, we skip the tool node because we don’t need it.
bookingUiNode
When availabilityAgentNode spawns bookingUiNode, the graph execution shifts client side.
WorkflowRenderer renders the UI node, and we can access all the required tools to observe and control the graph from the useWorkflowHook.
Check out Hooks from Learn section for more information.
'use client';
import { useState} from 'react';
import { useWorkflow } from '@cascaide-ts/react';
export default function BookingUi( { nodeId }: { nodeId: string }) {
const {addActiveNode, signalCompletion, nodeData} = useWorkflow(nodeId);
const cascadeId = nodeData.initialContext.cascadeId;
const [pin, setPin] = useState('');
const handleSubmit = async (e: any) => {
e.preventDefault();
const toolResponse = {
"role":"tool",
"tool_call_id": nodeData.initialContext.history.tool_calls[0].id,
"content": "Booking confirmed, booking id is : azzdfgr146"
}
await addActiveNode('bookingAgentNode', {
cascadeId: cascadeId,
history: [toolResponse],
userId: "guest-id"
});
await signalCompletion({nodeId,hasSpawns:true});
};
return (
/* Backdrop Container: Fixed to the full viewport, z-index ensures it stays on top */
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="w-full max-w-sm p-8 bg-white rounded-2xl shadow-2xl transform transition-all scale-100">
<h2 className="text-2xl font-bold text-center text-gray-800 mb-6">
Enter PIN
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="password"
inputMode="numeric"
maxLength={6}
value={pin}
onChange={(e) => setPin(e.target.value.replace(/\D/g, ''))}
placeholder="••••••"
className="w-full px-4 py-3 text-center text-2xl tracking-widest border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
required
autoFocus // Automatically focus for better UX
/>
</div>
<button
type="submit"
className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-md transition-colors duration-200"
>
Submit
</button>
</form>
<p className="mt-4 text-xs text-center text-gray-500">
Please enter your security code to continue.
</p>
</div>
</div>
);
}We access the information we need from nodeData which contains the context you spawned the bookingUiNode with
and we use addActiveNode to restart the cascade with the tool response.
Chat
We are using the prebuilt Chat UI. Check out UI/UX section for a deep dive into it.
What’s important for the purpose of this guide is to explain how Chat participates in the graph, handling delegation and present_hotel_options HITL UI.
MessageList
MessageList component in Chat looks for tool calls beginning with delegate_to, looks up the corresponding node name
and starts the sub agent cascade.
await addActiveNode(subNodeName, {
cascadeId: newSubCascadeId,
history: [{ role: 'user', content: query.trim() }],
originalToolCallId: toolCall.id,
userId,
});
It then monitors the sub cascade for completion (waiting for all sub cascades if parallel delegation happens) and sends the last message of the subcascade back to the supervisor as tool response. This is easily hackable, you can modify it as you see fit.
MessageBubble
This component is responsible for rendering messages. There is a tool registry where you can register your tool UIs.
export type ToolComponentProps = {
args: any;
onComplete: (result: string) => void;
isFinished: boolean;
savedResult: string | null;
};
export const toolRegistry: Record<string, React.ComponentType<ToolComponentProps>> = {
// ---
// Example input tool: user selects a hotel, cascade continues with their choice.
// Replace or extend this with your own tools.
// ---
present_hotel_options: HotelOptions,
};
If the tool UI is a simple display, it will render it. If it’s a HITL UI node, you can use the onComplete method to send the tool response back.
For a deep dive please look at Prebuilt Chat UI section of UI/UX. The component code itself is heavily commented. We have aimed to provide patterns and a starting point to make it your own.
Now we have all the nodes we need.
Defining Graphs and Configurations
Client Workflow Graph
Just the node meta data. Check out Defining Graphs and Configurations in Learn section.
import { ClientWorkflowGraph } from "@cascaide-ts/core";
export const clientWorkflowGraph: ClientWorkflowGraph = {
chat: { name: 'chat', isUINode: true ,env:'client'},
bookingUiNode: { name: 'bookingUiNode', isUINode: true,env:'client'},
supervisorAgentNode: { name: 'supervisorAgentNode', isUINode: false,env:'server' },
supervisorToolNode: { name: 'supervisorToolNode', isUINode: false,env:'server' },
bookingAgentNode: { name: 'bookingAgentNode', isUINode: false,env:'server' },
bookingToolNode: { name: 'bookingToolNode', isUINode: false,env:'server' },
availabilityAgentNode: { name: 'availabilityAgentNode', isUINode: false,env:'server' },
availabilityToolNode: { name: 'availabilityToolNode', isUINode: false,env:'server' },
}Client Workflow Configuration
In addition to the graph, we set up the uiComponent registry.
import Chat from "@/components/cascaide-ui/chat/chat"
import { ClientWorkflowConfig } from '@cascaide-ts/react';
import BookingUi from "@/components/cascaide-ui/bookingUiNode";
import { clientWorkflowGraph } from "./graph";
export const clientWorkflowConfig: ClientWorkflowConfig = {
clientWorkflowGraph: clientWorkflowGraph,
uiComponentRegistry: {
chat: Chat,
bookingUiNode : BookingUi,
},
};Server Workflow Graph
We declare the node meta data and the prep, exec, post functions only for the server nodes.
import { supervisorAgentNodeExec, supervisorAgentNodePost,
supervisorAgentNodePrep, supervisorToolNodeExec, supervisorToolNodePost,
supervisorToolNodePrep} from '@/bubbles/hotel-booking-agent/supervisorAgentNodes';
import {
bookingAgentNodePrep,
bookingAgentNodeExec,
bookingAgentNodePost,
} from '@/bubbles/hotel-booking-agent/bookingAgentNodes';
import { availabilityAgentNodeExec, availabilityAgentNodePost, availabilityAgentNodePrep, availabilityToolNodeExec, availabilityToolNodePost, availabilityToolNodePrep} from '@/bubbles/hotel-booking-agent/availabilityAgentNodes';
import { ServerWorkflowGraph } from '@cascaide-ts/core'
export const serverWorkflowGraph: ServerWorkflowGraph = {
supervisorAgentNode: {
name: 'supervisorAgentNode',
prep: supervisorAgentNodePrep ,
exec: supervisorAgentNodeExec,
post: supervisorAgentNodePost,
isStreaming: false,
isUINode:false,
env:'server'
},
bookingAgentNode: {
name: 'bookingAgentNode',
prep: bookingAgentNodePrep ,
exec: bookingAgentNodeExec,
post: bookingAgentNodePost,
isUINode:false,
isStreaming:false,
env:'server',
},
availabilityAgentNode: {
name: 'availabilityAgentNode',
prep: availabilityAgentNodePrep,
exec: availabilityAgentNodeExec,
post: availabilityAgentNodePost,
isStreaming:true,
env:'server',
isUINode:false
},
availabilityToolNode: {
name: 'availabilityToolNode',
prep: availabilityToolNodePrep,
exec: availabilityToolNodeExec,
post: availabilityToolNodePost,
isStreaming:false,
env:'server',
isUINode:false
},
searchAgentNode: {
name: 'searchAgentNode',
prep: searchAgentNodePrep,
exec: searchAgentNodeExec,
post: searchAgentNodePost,
isStreaming: true,
isUINode:false,
env:'server'
},
searchToolNode: {
name: 'searchToolNode',
prep: searchToolNodePrep,
exec: searchToolNodeExec,
post: searchToolNodePost,
isUINode:false,
isStreaming:false,
env:'server'
},
};Server Workflow Configuration
import { WorkflowHandlerConfig } from '@cascaide-ts/server-next'
import { PostgresPersistor } from '@cascaide-ts/postgres-js';
import { sql } from '@/lib/connection';
import { serverWorkflowGraph } from './graph';
const workflowpersistor = new PostgresPersistor(sql);
const MAX_EXECUTION_TIME = 100000;
const SAFE_BUFFER = 6000;
export const serverWorkflowConfig: WorkflowHandlerConfig = {
workflowGraph: serverWorkflowGraph,
persistor: workflowpersistor, // The user passes their PostgresPersistor here
maxExecutionTime: MAX_EXECUTION_TIME,
safeBuffer: SAFE_BUFFER
}For more info on the persistor, read QuickStart/Persistent Database.
For more info on the MAX_EXECUTION_TIME and SAFE_BUFFER read Deployment/NextJS.
Setting Up WorkflowHandler and WorkflowProvider
Action Endpoint
import { serverWorkflowConfig } from '@/graphs/server/config'
import { createWorkflowHandler } from '@cascaide-ts/server-next'
export const POST = createWorkflowHandler(serverWorkflowConfig);WorkflowProvider
'use client'
import { clientWorkflowConfig } from '@/graphs/client/config';
import { WorkflowProvider, WorkflowRenderer } from '@cascaide-ts/react';
export default function HomePage() {
return (
<WorkflowProvider
initialNodeId="chat_init"
initialNodeName="chat"
config={clientWorkflowConfig}
actionRelayEndpoint='/api/workflow/action'
hydrationEndpoint='/api/workflow/hydrate'
persistenceEndpoint='/api/workflow/persistence'
>
<WorkflowRenderer />
</WorkflowProvider>
);
}We are assuming you have already set up the hydration and persistence endpoints as per the quickstart guide.
Now your Cascaide application is set up. Run it using npm run dev and open localhost 3000. Do not forget to set your OpenAI API key and Database URL for the persistent database in your environment variables.