Lesson 3: Flow Shell & Engine Loop
Meet BasicFlow
src/myflow/basic-flow/basic-flow.ts defines the shell. The Flow constructor sets a flow-wide default model so you don’t repeat it on every step:
export class BasicFlow extends Flow {
constructor() {
super(BasicFlow);
this.useModel('gpt-4o');
}
protected defineSteps(): Step[] {
const isPresident = this.getContext<boolean>('config.isPresident');
return [
new WeatherStep(this, !isPresident),
new NameStep(this).useMemory('default'),
new AddressStep(this).useMemory('default'),
new DOBStep(this).useMemory('default'),
new FooLogicStep(this).useMemory('default'),
new GooLogicStep(this).useMemory('default'),
new InContextStep(this).useMemory('separate').useModel('gpt-5', {
reasoning: { effort: 'high' },
}),
new PresidentStep(this, isPresident).useMemory('president'),
new FavoritesStep(this).useMemory('favorite'),
new EndStep(this).useMemory('temp'),
];
}
}
Tip
The order of steps is deterministic — a single list, not an explicit graph. Transitions are driven by tool results and logic steps.
Session Document Lifecycle
Each HTTP turn:
- Loads the session document by
CHAT_SESSION_ID. - Hydrates memory for the active step only.
- Calls the LLM with that step’s prompt + tools.
- Executes any tool calls synchronously.
- Persists memory, step state,
sequence, tokens, and logs back to Mongo/Cosmos.
Because state is in Mongo, any container can handle the next turn.
Controller Wiring
Expose /ai/chat and pass control to the FlowEngine:
@Controller('ai')
export class TutorialController {
constructor(private flowEngine: FlowEngine) {
flowEngine.registerFlows({ BasicFlow });
flowEngine.registerModel(ChatOpenAI, { model: 'gpt-4o', apiKey: process.env.OPENAI_KEY });
}
@Post('chat')
async chat(
@Res() res: FastifyReply,
@Body('message') message: string,
@Body('flowName') flowName: string,
@Headers('CHAT_SESSION_ID') sessionId?: string,
@Body('config') config?: object,
) {
await this.flowEngine.run(res, flowName, message, sessionId, config);
}
}
Why This Matters
- Flow shell owns registry, model defaults, and deterministic step list.
- The engine’s recursive loop (LLM → tools → transitions → LLM) is hidden behind
run, so the controller stays tiny. - Memory is per-step, keeping prompts clean and avoiding cross-talk.
Next we’ll dive into the first step: multi-call capture with validation.