@@ -182,6 +182,16 @@ async def generate_title(state: "State", config: RunnableConfig) -> dict:
182182 "ask for a UI / form / card / interactive surface."
183183)
184184
185+ _PLANNER_FRAMING = (
186+ "\n \n --- APP MODE: TRIP PLANNER ---\n "
187+ "You are a trip-planning assistant driving a live map cockpit. When the user "
188+ "asks for a plan, RECOMMEND concrete places (a short note each) grouped into "
189+ "days, and POPULATE the itinerary by calling `add_stop` for each recommendation "
190+ "(then `day_card` to recap a day). Revise with `move_stop`/`reorder_stop`/`clear_day`. "
191+ "Do NOT just describe the plan in prose — call the tools so the map and panel update. "
192+ "Only add stops that are not already present."
193+ )
194+
185195# Reasoning-capable model prefixes. We only attach the ``reasoning``
186196# parameter when the model name suggests reasoning support; setting it
187197# on a non-reasoning model would be ignored anyway.
@@ -391,6 +401,25 @@ class State(TypedDict):
391401 itinerary : list [Stop ]
392402
393403
404+ def build_system_prompt (gen_ui_mode : str , client_tools : list , itinerary : list ) -> str :
405+ system = SYSTEM_PROMPT
406+ if gen_ui_mode == "a2ui" :
407+ system = system + "\n \n --- A2UI v1 SCHEMA ---\n " + A2UI_V1_SCHEMA_PROMPT + (
408+ "\n \n When rendering UI in a2ui mode, emit envelopes in this order: "
409+ "surfaceUpdate FIRST, then beginRendering, then any dataModelUpdate "
410+ "entries. This lets the client mount the surface as early as possible."
411+ )
412+ if client_tools :
413+ system = system + _PLANNER_FRAMING
414+ if itinerary :
415+ import json as _json
416+ system = system + (
417+ "\n \n CURRENT ITINERARY (do not re-add existing stops):\n "
418+ + _json .dumps (itinerary , ensure_ascii = False )
419+ )
420+ return system
421+
422+
394423async def generate (state : State , config : RunnableConfig ) -> dict :
395424 model_name = state .get ("model" ) or "gpt-5-mini"
396425 kwargs = {"model" : model_name , "streaming" : True }
@@ -434,14 +463,14 @@ async def generate(state: State, config: RunnableConfig) -> dict:
434463 state ,
435464 )
436465 # Append A2UI v1 schema to system prompt when in a2ui mode, so the parent
437- # LLM knows how to construct the envelopes directly.
438- system = SYSTEM_PROMPT
439- if gen_ui_mode == "a2ui" :
440- system = SYSTEM_PROMPT + " \n \n --- A2UI v1 SCHEMA --- \n " + A2UI_V1_SCHEMA_PROMPT + (
441- " \n \n When rendering UI in a2ui mode, emit envelopes in this order: "
442- "surfaceUpdate FIRST, then beginRendering, then any dataModelUpdate "
443- "entries. This lets the client mount the surface as early as possible."
444- )
466+ # LLM knows how to construct the envelopes directly. When the frontend
467+ # client-tool catalog is present (App mode), also inject the trip-planner
468+ # framing + the current itinerary so the model populates state via the tools.
469+ system = build_system_prompt (
470+ gen_ui_mode = gen_ui_mode ,
471+ client_tools = state . get ( "client_tools" ) or [],
472+ itinerary = state . get ( "itinerary" ) or [],
473+ )
445474 messages = [SystemMessage (content = system )] + state ["messages" ]
446475 # When in a2ui mode, attach the partial-envelope sideband handler so
447476 # the parent LLM's tool_call_chunks for render_a2ui_surface are
0 commit comments