Compare
One turn, traced twice
Follow “compare hotel 2, 5, and 8 on price” from HTTP request to rendered table in both implementations.
A feature matrix can hide the engineering cost of one ordinary turn. This trace uses the
scenario request compare hotel 2,5,8 on price, issued while the user is looking at search
results.
Before the request
Both implementations already hold the same essential domain data:
- the nine priced search results;
- the ordered hotel names shown to the user;
- the selected date range and per-day prices; and
- stage-specific model history for presenting results.
Their cursors differ. PicoFlow persists flow.currentStep = "PresentStep". The direct graph
persists phase = "present"; each new HTTP turn enters the compiled graph at START, where
routeFromPhase() selects presentAgent.
PicoFlow trace
POST /ai/run
-> FlowEngine.run({ flowName, userMessage, sessionId })
-> restore and lock the PicoFlow session
-> PresentStep.run(user message)
-> model calls go_compare
-> decorated PresentStep.go_compare(args)
-> save selected names in CompareStep state
-> go(CompareStep).withState(...).withMessage(original request)
-> framework exits PresentStep and enters CompareStep
-> CompareStep.onEnter() clears its prior visit's memory
-> model calls generate_comparison
-> decorated CompareStep.generate_comparison(args)
-> fetch domain data and render Markdown
-> direct(table) returns without another model call
-> save the common session document with compare-and-swap
The application expresses the transition in its handler:
return go(CompareStep)
.withState({ available_hotel: strAvailableHotel })
.withMessage(this.getLastMessage());
PicoFlow supplies the remaining protocol: finding the decorated handler, matching tool-call IDs, switching the cursor, applying destination state, delivering the forwarded message, recognizing the direct response, serializing memory, and saving the session.
There are two model calls in the turn: one in PresentStep to recognize the transition and one
in CompareStep to choose the comparison tool. direct() prevents a third call after the
deterministic table is ready.
Direct LangGraph trace
POST /ai-langgraph/run
-> HotelLanggraph.run({ userMessage, sessionId })
-> custom store.get(sessionId), then hydrate messages
-> compiledGraph.invoke(update, { recursionLimit: 50 })
-> START conditional edge reads phase and selects presentAgent
-> presentAgent calls the bound model
-> conditional edge sees a tool call and selects presentTools
-> presentTools manually dispatches go_compare
-> validate selected names and update phase/route/message state
-> conditional edge selects compareAgent
-> compareAgent calls the bound model
-> conditional edge selects compareTools
-> compareTools validates generate_comparison, renders Markdown
-> route = end sends execution to END
-> serialize messages and custom store.set(document)
The equivalent transition is a state update:
return {
compareMessages: new HumanMessage(
"Choose hotels and one feature to compare.",
),
availableHotels: available,
selectedHotels: selected,
phase: "compare",
inputConsumed: false,
response: "",
route: "compareAgent",
};
The original request remains in state.userInput. inputConsumed: false tells
compareAgent to append it to the comparison history, while the synthetic message establishes
the new stage. Again there are two model calls; compareTools places the deterministic table
directly in state.response and routes to END.
What the trace reveals
| Concern | PicoFlow | Direct LangGraph |
|---|---|---|
| Durable cursor | currentStep |
phase |
| Within-turn cursor | Framework execution stack | route plus conditional edges |
| Input forwarding | withMessage(...) |
userInput, inputConsumed, synthetic message |
| Tool dispatch | Framework finds decorated handler | Tool node branches on call.name |
| Destination state | withState(...) |
Node update object |
| Exact response | direct(...) |
response update plus route: "end" |
| Persistence | Framework session save | Application serialization and store write |
The direct version is more mechanically visible: every state update and route can be stepped through locally. PicoFlow is more semantically compact: the application says what transition means and delegates the execution protocol. That is the core trade-off repeated throughout the larger comparison.
Latency and model cost
For this turn the orchestration choice does not change the dominant model cost: both make two serial model calls before returning the table. The direct graph does not become faster merely because it has explicit nodes, and PicoFlow does not remove an inference merely because its transition syntax is shorter.
PicoFlow may make an additional summary-model call when the configured explore memory crosses its compaction threshold; that work does not occur in this comparison turn. The direct graph never summarizes, trading lower immediate summarization cost for histories that grow without a bound.
Measure model calls, tokens, tool latency, storage round trips, and p95 end-to-end latency per user intent. Source lines are an ownership metric, not a runtime-performance benchmark.