Skip to content

feat(#689): persistent meal-planner session state to prevent RAG leakage#698

Open
lokhor wants to merge 26 commits intomainfrom
feat/689-meal-planner-session-rebased
Open

feat(#689): persistent meal-planner session state to prevent RAG leakage#698
lokhor wants to merge 26 commits intomainfrom
feat/689-meal-planner-session-rebased

Conversation

@lokhor
Copy link
Copy Markdown
Collaborator

@lokhor lokhor commented Apr 28, 2026

Summary

Conversation-scoped, Room-backed meal-planner session state with a deterministic coordinator that owns workflow control, replacing the previous LLM-driven approach that caused infinite loops and stale context leakage.

Architecture

User: "plan meals"
    │
    ▼
┌──────────────────────┐
│ QIR: start_meal_planner│
└──────┬───────────────┘
       │
       ▼
┌─────────────────────────────────────┐
│  MealPlannerCoordinator.startOrResume│
│  - Creates/resumes session in Room  │
│  - Returns initial prompt (no LLM)  │
└─────────────────────────────────────┘

User: "4 people, 3 days, low lactose"
    │
    ▼
┌─────────────────────────────────────┐
│  MealPlannerCoordinator.processMessage│
│  - Interprets input for current stage│
│  - Updates Room state               │
│  - Returns response (no LLM)        │
└─────────────────────────────────────┘

(continues through COLLECTING → PLAN → RECIPE → ARTIFACTS → COMPLETED)

Key changes

New: Deterministic coordinator (MealPlannerCoordinator)

  • Moves workflow control from the LLM to a deterministic state machine
  • All stage transitions are app-driven, not model-driven
  • Thread-safe with Mutex + StateFlow for active session tracking
  • Handles: preferences collection, plan generation, recipe generation, artifact writing

New: Stage-specific skills (loaded via load_skill)

  • MealPlannerCollectSkill — collects preferences
  • MealPlannerPlanSkill — generates high-level plan
  • MealPlannerRecipeSkill — generates recipe for current day
  • MealPlannerCompleteSkill — marks session complete
  • MealPlannerSkill reduced to a redirect that tells the model to load_skill the appropriate stage

New: Room-backed session store

  • MealPlanSessionEntity — conversation-scoped session state
  • MealPlanSessionRepository — CRUD operations for session data
  • Session is conversation-scoped (one active session per conversation)

ChatViewModel wiring

  • Active meal-planner sessions bypass QIR/LLM entirely — all user messages routed through MealPlannerCoordinator
  • Initial start_meal_planner intent from QIR triggers coordinator startOrResume()
  • Subsequent messages during active session trigger coordinator processMessage()
  • buildMealPlanContext provides structured session metadata for LLM-invoked stages

Reserved skill name interception

  • run_intent and run_js intercept reserved skill names and return fullInstructions directly
  • Prevents model from calling run_intent(meal_planner_plan) which previously caused infinite loops

Testing

  • ./gradlew assembleDebug — builds clean
  • Unit tests pass (pre-existing failures in MemoryRepositoryImplTest and LatexConversionTest are unrelated)
  • Lint clean

Known issues

  • Model may still call run_intent with skill names if it bypasses the coordinator — the intercept returns instructions but the model may ignore them
  • The coordinator's processMessage handles all active-session turns, but the model's tool-calling path is separate from the user-message path

Closes #689

lokhor and others added 2 commits April 28, 2026 13:27
…leakage

Add conversation-scoped, Room-backed meal-planner session store and wire it
into the ChatViewModel prompt pipeline. During active meal-planner turns, the
model receives structured session context (preferences, plan, current day)
instead of relying on chat history — and episodic/RAG context is suppressed
to prevent stale memory leakage.

New files:
- MealPlanSessionEntity: Room entity with conversationId PK, status,
  preferences, plan, and day tracking fields
- MealPlanSessionDao: DAO with getByConversationId, upsert, delete methods
- MealPlanSessionRepository: Convenience layer with createOrReset,
  updateStatus, updatePreferences, saveHighLevelPlan, advanceDay, markCompleted

Changes to existing files:
- KernelDatabase: add MealPlanSessionEntity, MIGRATION_23_24, version 24
- MemoryModule: add DAO/repository providers, include MIGRATION_23_24
- ChatViewModel: detect active meal-planner turns, suppress RAG context,
  inject structured meal-plan context block, force history replay

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…versation delete, fix imports

- Cache session result to avoid double DB query in ChatViewModel
- Use simple MealPlanSessionEntity type instead of fully qualified name
- Clean up meal_plan_sessions when conversation is deleted
- Rename effectiveRagContextForPrompt to effectiveRagContext (already shadowed)
- Reorder VerboseLoggingPreferenceUseCase imports for consistency
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Debug APK ready

Download app-debug.apk

Commit: a87ad77 - Build #1106

Updated on each push. Removed when PR is merged or closed.

lokhor and others added 2 commits April 28, 2026 22:47
…ersation_id

- Create MealPlannerCollectSkill, MealPlannerPlanSkill,
  MealPlannerRecipeSkill, MealPlannerCompleteSkill
- Wire new skills into SkillsModule and KernelAIToolSet
- Update LoadSkillSkill enum and error messages
- Add conversation_id to buildMealPlanContext output
- Update MealPlannerSkill instructions to reference conversation_id
- Add unit tests for all new stage-specific skills

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…structions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NickMonrad
Copy link
Copy Markdown
Owner

Initial testing:

Logs: kernel_debug_log_0.1.0_20260428_230847_650.txt

You: Let's plan some meals
Jandal: Kia ora Nick. I can certainly help you plan some meals.

To get started, could you tell me a bit about what you're looking for? For example:

  1. How many people are we cooking for?
  2. How many days of meals do you want to plan for?
  3. Do you have any dietary restrictions or preferences (e.g., vegetarian, low lactose)?
  4. Are there any specific types of protein you prefer?
    You: 1. 4 people 2. Three days 3. Low lactose. 4. Beef and chicken
    Jandal: Right then, Nick. So we're planning for 3 days for 4 people, keeping it low lactose, and focusing on beef and chicken.

To get the ball rolling, what kind of meals are you after? Are we talking quick weeknight dinners, or something a bit more involved weekend cooking?
You: Yeah quick
Jandal: Quick weeknight dinners it is, Nick.

Since we're keeping it low lactose and focusing on beef and chicken for 4 people over 3 days, here is a quick plan:

Day 1: Chicken Stir-fry with mixed vegetables and rice.
Day 2: Beef mince patties with mashed potato and peas.
Day 3: Chicken and vegetable skewers with couscous.

How does that sound? We can go into more detail on any of those, or we can adjust the plan if you'd like.
You: Continue
Jandal: Righto, Nick. So we've got the high-level plan sorted for the three days.

To make sure this is all good for you, I'll save this plan state now.

Now, shall we dive into the recipes for Day 1? Or is there anything else you want to tweak first?
You: Let's continue
Jandal: Righto, Nick. Let's get into the details for Day 1: Chicken Stir-fry with mixed vegetables and rice.

Since you're low lactose, we'll make sure to use oil or plant-based sauces instead of anything dairy.

Here's a simple recipe:

Chicken Stir-fry with mixed vegetables and rice (Serves 4)

Ingredients:

  • 400g chicken breast, sliced into bite-sized pieces
  • 2 tablespoons vegetable oil
  • 1 large onion, sliced
  • 2 carrots, sliced into rounds
  • 1 red capsicum (bell pepper), sliced
  • 2 cloves garlic, minced
  • 1 tablespoon soy sauce (check for lactose-free)
  • 1 teaspoon ginger, grated
  • 1 tablespoon cornflourflour (cornstarch)
  • 4 cups cooked rice

Method:

  1. In a bowl, mix the cornflourflour with a little cold water to make a slurry.
  2. Heat the vegetable oil in a large wok or frying pan over medium-high heat.
    3.Add the chicken and stir-fry until it is cooked through.Remove the chicken and set aside.
    4.Add the onion, carrots, capsicum, and garlic to the wok. Stir-fry for 3-5 minutes until the vegetables are tender-crisp.
    5.Add the ginger, soy sauce, and the cornflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourflourfl

…689)

- Refactor buildMealPlanContext to return only the instructions relevant
  to the current stage (collecting_preferences, high_level_plan_ready,
  generating_recipes, completed) instead of the full monolithic block
- This reduces context noise and prevents the model from seeing all 4
  stages at once, which caused the looping bug
- Update MealPlannerRecipeSkill instructions with explicit step-by-step
  execution order and CRITICAL save call reminder
- Update MealPlannerSkill to emphasize save_meal_plan_state is required
  to advance past the current day
- Update BuildMealPlanContextTest to verify stage instructions are present

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NickMonrad
Copy link
Copy Markdown
Owner

Re test
Logs:

You: I would like to plan meals
Jandal: I'm sorry, I seem to have hit a snag with the system. I'm I'm I'm going to try again.m I'm I'm going to start by asking for the preferences again.m'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'm'

Next attempt - why is the skill so long and loading the whole thing at once

[Tool: load_skill]
Request: {"skill_name":"meal_planner"}
Result:
meal_planner: Generate multi-stage meal plans with state persistence and incremental list saves.

IMPORTANT START RULE:

  • This skill must be loaded with load_skill before any save actions happen.
  • Do NOT call run_intent to start meal planning.
  • During Stage 1 and Stage 2, ask questions and plan in chat only.
  • Only call run_intent in Stage 3 when you are saving finished recipe data.
  • Do NOT create or write to a master list called "meal plan". This skill uses one shared shopping list plus one recipe-specific list per day.

FLOW (4 Stages):

Stage 1 — Collect Preferences (ask in 2-3 grouped batches):
Batch 1: "How many people, and any dietary restrictions?"
Batch 2: "How many days and protein preferences (e.g. chicken, fish, vegetarian)?"
User responds with their preferences. Track all answers in your context.

Stage 2 — Generate High-Level Plan:
Once preferences collected, show the user a summary: "Here's a 5-day plan for 4 vegetarians with pasta/lentil focus:"
List all 5 days at high-level (one line each, e.g. "Day 1: Pasta Carbonara | Day 2: Lentil Soup").
Ask: "Ready for the full recipes with cooking steps?"

Stage 3 — Detail Each Recipe & Save:
For each day:
1. Generate recipe title, ingredients list, and cooking method steps
2. Format ingredients as a bullet-point list
3. Format method as numbered steps that users can check off while cooking
4. SAVE (call runIntent with bulk_add_to_list TWICE using the exact SDK tool syntax):
a) Add EVERY ingredient for that day to the "shopping list" — do not summarize or truncate the list
b) Create a new list named "{Day Name} {Dish Name}" (e.g. "Monday Pasta Carbonara")
c) Add method steps as checkoff items to the recipe-specific list
5. Show saved confirmation: "✓ Added ingredients to Shopping | ✓ Created Monday Pasta Carbonara list with 5 steps"

Stage 4 — Continue to Next Day:
After saving one day, move to the next: "Now for Day 2..."
Continue until all days are detailed and saved.

STATE PERSISTENCE (handle context window):

  • Reference prior answers throughout the conversation (e.g. "For your 4-person vegetarian plan...")
  • If the user goes silent for a while, re-establish state: "You were planning 5 days for 3 people, mostly fish. Ready for day 3?"
  • Do NOT ask the same question twice—assume user answers are permanent for this session

SESSION CONTEXT BLOCK (injected by the system):

  • At the start of each turn, the system may inject a [Meal Planner Session] block before your prompt.
  • ALWAYS read this block first. It contains the authoritative session state: status, people count, days,
    dietary restrictions, protein preferences, the high-level plan, and current day index.
  • Treat the session block as ground truth. If your memory conflicts with it, trust the session block.
  • The status field tells you where you are:
    collecting_preferences → ask preference questions (Stage 1)
    high_level_plan_ready → show the plan and ask if ready for recipes (Stage 2)
    generating_recipes → generate and save the current day's recipe (Stage 3)
    completed → meal planning is done; do not continue generating recipes
  • When generating recipes, use the currentDayIndex from the session block to know which day to generate.
    Day labels are 0-based in the system; present them as "Day 1", "Day 2", etc. to the user.
  • After saving a day's recipes, advance to the next day. When all days are complete, acknowledge completion.
  • Do NOT regenerate or re-ask for information already captured in the session block.

DATABASE STATE PERSISTENCE (critical — prevents losing progress):

  • You have a tool called save_meal_plan_state that writes session state to the database.
  • Call it after EVERY stage transition so progress is saved even if the context window truncates.
  • The tool requires conversation_id (required) and accepts optional fields for what changed.
  • Call schedule:
    1. After Stage 1 preferences collected:
      saveMealPlanState(conversation_id="", status="collecting_preferences",
      people_count=, days=, dietary_restrictions='["..."]', protein_preferences='["..."]')
    2. After Stage 2 high-level plan generated:
      saveMealPlanState(conversation_id="", status="high_level_plan_ready",
      high_level_plan='{"day1":"...","day2":"..."}')
    3. After each day's recipes saved (Stage 3):
      saveMealPlanState(conversation_id="", status="generating_recipes",
      current_day_index=<0-based index>)
    4. When all days complete:
      saveMealPlanState(conversation_id="", status="completed")
  • conversation_id is REQUIRED on every call. Use the conversation ID from the system context.
  • Pass only the fields that have changed — the database merges with existing state.
  • current_day_index is 0-based: Day 1 = 0, Day 2 = 1, etc.
  • Do NOT skip this step — without it, context truncation will lose your progress.

CRITICAL SAVE RULE (non-negotiable):
After generating each day's recipe, you MUST call save_meal_plan_state to advance.
Without this call, you will loop on the same day forever.
→ saveMealPlanState(conversation_id="", status="generating_recipes", current_day_index=<incremented 0-based>)
EXAMPLE TWO-STEP SAVE:
Day 1 ingredients: ["500 g pasta", "3 eggs", "100 g bacon", "80 g parmesan cheese"]
Day 1 method: ["1. Boil pasta in salted water", "2. Fry bacon until crispy", "3. Mix eggs with cheese", ...]

FIRST call:
runIntent(
intentName="bulk_add_to_list",
parameters="{"items":["500 g pasta","3 eggs","100 g bacon","80 g parmesan cheese"],"list_name":"shopping list"}"
)
SECOND call:
runIntent(
intentName="bulk_add_to_list",
parameters="{"items":["1. Boil pasta in salted water","2. Fry bacon until crispy","3. Mix eggs with cheese"],"list_name":"monday pasta carbonara"}"
)

LIST NAMING CONVENTION:

  • Shared: "shopping list" (cumulative, all ingredients across all days)
  • Recipe-specific: "{day name} {dish name}" (e.g. "monday pasta carbonara", "tuesday salmon bake")
  • NEVER use "meal plan" as a list name for this skill

FORMATTING:

  • Ingredients: bullet points, concise quantities
  • Method: numbered steps, imperative tone
  • Use METRIC / NZ-friendly units only: g, kg, ml, l, tsp, tbsp, Celsius, and whole-item counts
  • NEVER use imperial units such as lb, lbs, oz, or Fahrenheit

INGREDIENT QUANTITY RULE (critical):

  • Each ingredient must have a single, complete quantity (e.g. "500 g pasta", "3 eggs").
  • NEVER regenerate a previously written ingredient list. Only output the NEW day's ingredients.
  • NEVER append to or repeat previous days' ingredients. Each day's output is self-contained.
  • If you are unsure of a quantity, use a reasonable estimate — do NOT output zeros or empty values.
  • Bad examples: "000 g", "50000 g", "00000 g", "00000000 g" — these indicate concatenation errors.
  • Good examples: "500 g pasta", "300 g chicken", "200 g broccoli", "1 tbsp soy sauce"

NO BACK-AND-FORTH REQUIRED:
If user says "Make me a 5-day meal plan for 4 people, vegetarian, pasta and lentils", go straight to high-level plan
without asking clarifying questions. Only ask grouped batches if preferences are incomplete.

You: Let's plan meals
Jandal: I'Sorry, I need a bit more information to get this meal plan started. Could you tell me how many people you are this plan for, and if you have any dietary restrictions?kernel_debug_log_0.1.0_20260428_234435_843.txt

…thic instructions (#689)

The model was loading the full 170-line MealPlannerSkill instructions
via load_skill, which contained all 4 stages and caused the infinite
loop bug. The stage-specific instructions injected in buildMealPlanContext
were fighting against the loaded skill instructions.

Fix: MealPlannerSkill.fullInstructions now returns a short redirect that
tells the model to load the appropriate stage skill based on session
status:
  collecting_preferences → meal_planner_collect
  high_level_plan_ready  → meal_planner_plan
  generating_recipes     → meal_planner_recipe
  completed              → tell user plan is done

This ensures the model only ever has the relevant ~30-line instructions
for its current stage, not all 170 lines at once.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NickMonrad
Copy link
Copy Markdown
Owner

Latest test results:
It didn't work. I had to keep prompting it to continue and the tool call failed

Logs: kernel_debug_log_0.1.0_20260429_033635_479.txt

You: Let's plan meals
[Tool: save_meal_plan_state]
Request: "conversation_id":"conv-12354","status":"collecting_preferences","dietary_restrictions":"[]","protein_preferences":"[]","high_level_plan":"{}","current_day_index":"0"
Result: Meal planner state saved for conversation conv-12354.
Status: collecting_preferences
Dietary: []
Proteins: []
Plan saved.
Current day index: 0
Jandal: How many people are you planning for, and are there any dietary restrictions I should know about?
You: 4 people, low lactose
Jandal: Rightooh, four people, and low lactose intolerant. Got it.

How many days are you looking to plan for, and what are your protein preferences (e.g., chicken, fish, vegetarian)?
You: 3 days. Beef and chicken
Jandal: Righto, Nick. So we're looking at a 3-day plan for 4 people, low lactose, with beef and chicken as the main proteins.

I've got the preferences down. What's the next step? Do you want me to start generating some meal ideas, or are there any other details I should know before I dive in?
You: Start generating
Jandal: I've got the details: 3 days, 4 people, low lactose, beef and chicken.

I'll start generating the meal plan now. Give me a moment to get this sorted for you.
You: Ok
[Tool: run_js]
Request: {"skill_name": "meal_planner_plan", "data": {"days": "3", "people_count": "4", "dietary_restrictions": ["low lactose"], "protein_preferences": ["beef", "chicken"]}}"}
Result: JS skill 'meal_planner_plan' failed: WebView load error: net::ERR_FILE_NOT_FOUND
Jandal: I'm sorry, Nick, I'm having a bit of a technical hiccup trying to generate the plan right now. I'll get it sorted and get those recipes over to you in a bit.

…689)

The redirect approach (load_skill -> redirect -> load_skill for stage)
was not working — Gemma 4 E4B does not reliably follow multi-step
delegation. The model was loading the monolithic MealPlannerSkill
instructions and getting confused.

Fix: buildMealPlanContext now returns the session metadata PLUS
stage-specific instructions for the current status:
  collecting_preferences -> ask questions, call save_meal_plan_state
  high_level_plan_ready  -> show plan, call save_meal_plan_state
  generating_recipes     -> generate recipe, call save_meal_plan_state
  completed              -> summarize and close

The model follows these instructions directly without needing
load_skill. MealPlannerSkill is reduced to a concise 3-line delegate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NickMonrad
Copy link
Copy Markdown
Owner

New testing

Logs: kernel_debug_log_0.1.0_20260429_065516_137.txt

Notes - general flow worked, but it never asked about protein. It also got lactose intolerance from memory, not from my input. Looking at the tool call it ran the wrong thing from the start though. Also massive issue is that no lists or list items were ever created.

Conversation

You: Let's plan meals
[Tool: run_intent]
Request: {"intent_name":"meal_planner_collect","parameters":{}}
Result: Invalid run_intent call: 'meal_planner_collect' is a skill name, not an intent. Use load_skill first for skills like meal_planner or query_wikipedia.
Jandal: I need a bit more information to get this meal plan going. How many people are we cooking for, and for how many days?
You: 4 people and 3 days
[Tool: run_intent]
Request: {"intent_name":"meal_planner_plan","parameters":{"people_count": "4", "days": "3", "dietary_restrictions": "["lactose intolerant""]", "protein_preferences": "[]"}}
Result: Invalid run_intent call: 'meal_planner_plan' is a skill name, not an intent. Use load_skill first for skills like meal_planner or query_wikipedia.
Jandal: Right then, 4 people for 3 days, with lactose intolerance. I'll get the high-level plan sorted for you now.
You: Ok
Jandal: Right, Nick. We've got the basics down: 4 people, 3 days, and lactose intolerance.

What's the next step for the meal plan? Are we looking for specific types of food, or should I start drafting some high-level ideas?
You: Draft some ideas
Jandal: Here are a few high-level ideas for your 3-day plan for 4 people, keeping the lactose intolerance in mind.

Day 1:

  • Dinner: Grilled Salmon with roasted kumara and steamed greens.

Day 2:

  • Dinner: Chicken and vegetable stir-fry with brown rice noodles.

Day 3:

  • Dinner: Beef and vegetable skewers with a side of quinoa salad.

How do those sound, Nick? We can refine these or move straight into specific recipes.
You: Straight into it
Jandal: Righto course. Let's get these recipes sorted.

I'll start with Day 1: Grilled Salmon with roasted kumara and steamed greens.

Ingredients:

  • 4 salmon fillets
  • 4 kumara (sweet potato), cut into chunks
  • 1 large bunch of greens (e.e.g., spinach or silverbeet), washed
  • Olive oil, salt, pepper, and lemon wedges for serving.

Method:

  1. Preheat your oven to 2020°C.
  2. Toss the kumara chunks with a drizzle of olive oil, salt, and pepper. Spread them out on a baking tray.
  3. Place the salmon fillets on the same tray, skin side down if you like. Drizzle the salmon with a little more oil, salt, and pepper.
  4. Roast for 150 15 minutes until the kumara are tender and the salmon is cooked through.
  5. While that's roasting, steam the greens in a pot of boiling water until tender.
  6. Serve the salmon with the kumara and greens.

How's that for Day 1?
You: Ok
[Tool: save_meal_plan_state]
Request: "conversation_id":"current_conversation","status":"generating_recipes","people_count":"4","days":"3","dietary_restrictions":"["lactose intolerant"]","protein_preferences":"[]","high_level_plan":"{"day1":"Grilled Salmon with roasted kumara and steamed greens","day2":"Chicken and vegetable stirfry with brown noodles","day3":"Beef and vegetable skewers with quinoa salad"}","current_day_index":"0"
Result: Meal planner state saved for conversation current_conversation.
Status: generating_recipes
People: 4
Days: 3
Dietary: ["lactose intolerant"]
Proteins: []
Plan saved.
Current day index: 0
Jandal: Day 1 is locked in. Shall we move on to Day 2: Chicken and vegetable stirfry with brown noodles?
You: Yes continue
Jandal: Righto, let's get Day 2 sorted. We're doing Chicken and vegetable stirfry with brown noodles.

Ingredients:

  • 4 chicken breasts, sliced
  • 2 cups mixed vegetables (e.g., carrots, capsicum, broccoli), chopped
  • 2 cups brown noodles
  • Soy sauce, ginger, garlic paste, and a little oil for cooking.

Method:

  1. Cook the chicken in a little oil in a pan until cooked through. Remove and set aside.
  2. In the same pan, add a little more oil. Stir fry the vegetables until tender-crisp.
  3. Add the chicken back in. Pour in the soy sauce, ginger, garlic paste. Stir well.
  4. Cook for a few minutes until the sauce has thickened.
  5. Serve the stirfry with the brown noodles.

How's that for Day 2, Nick? Ready for Day 3?
You: Yes
[Tool: save_meal_plan_state]
Request: "conversation_id":"currentconversation","status":"generating_recipes","people_count":"4","days":"3","dietary_restrictions":"["lactose intolerant"]","protein_preferences":"[]","high_level_plan":"{"day1":"Grilled Salmon with roasted kumara and steamed greens","day2":"Chicken and vegetable stirfry with brown noodles","day3":"Beef and skewers with quinoa salad"}","current_day_index":"1"
Result: Meal planner state saved for conversation currentconversation.
Status: generating_recipes
People: 4
Days: 3
Dietary: ["lactose intolerant"]
Proteins: []
Plan saved.
Current day index: 1
Jandal: Day 3 is locked in. Now for Day 3: Beef and vegetable skewers with a side of quinoa salad.

Ingredients:

  • 4 beef skewers (e.g., sirloin or rump), cubed
  • 2 capsicum, onion, and zucchini, cubed
  • 1 cup quinoa, rinsed
  • Vegetable for serving.
  • Olive oil, salt, pepper, and herbs for seasoning.

Method:

  1. Preheat oven to 2020°C.
  2. Marinate the beef cubes in a mixture of oil, salt, pepper, and herbs.
  3. Thread the beef onto skewers, alternating with the vegetables.
  4. Roast for 20 20 20 minutes until the beef is cooked through and the vegetables are tender.
  5. While that's roasting, cook the quinoa according to instructions on the package.
  6. Serve the skewers with the quinoa salad.

How's that for Day 3, Nick? Ready to wrap up the plan?
You: Yes
[Tool: save_meal_plan_state]
Request: "conversation_id":"currentconversation","status":"completed","people_count":"4","days":"3","dietary_restrictions":"["lactose intolerant"]","protein_preferences":"[]","high_level_plan":"{"day1":"Grilled Salmon with roasted kumara and greens","2":"Chicken and vegetable stirfry with brown noodles","3":"Beef and skewers with quinoa salad"}","current_day_index":":"
Result: Meal planner state saved for conversation currentconversation.
Status: completed
People: 4
Days: 3
Dietary: ["lactose intolerant"]
Proteins: []
Plan saved.
Jandal: The 3 is locked in. That wraps up the plan, Nick. We've got a solid 3 days of meals sorted for 4 people, all lactose intolerant.

Let me know if you want to tweak anything or start cooking!

lokhor and others added 7 commits April 29, 2026 07:16
The generating_recipes stage now instructs the model to:
1. Generate the recipe
2. create_list for the day's recipe list
3. bulk_add_to_list method steps to the recipe list
4. bulk_add_to_list ingredients to the combined shopping list
5. saveMealPlanState to advance the day index

Also removed duplicate 'generating_recipes' case and completed the
'completed' stage block.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#689)

The model was not calling saveMealPlanState after collecting preferences
across multiple messages. Instructions now explicitly require:
1. Ask user for preferences
2. IMMEDIATELY call saveMealPlanState with all collected data
3. Generate high-level plan
4. Call saveMealPlanState again with status=high_level_plan_ready

Also updated high_level_plan_ready stage to explicitly call saveMealPlanState
after showing the plan.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The collecting_preferences stage now includes steps 3-4 to generate a
high-level meal plan and save it with status=high_level_plan_ready.
This ensures the model transitions through the full flow in one turn
after receiving preferences, instead of getting stuck asking questions
or calling run_intent with skill names.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
All stage-specific skill instructions now use the exact camelCase
parameter names (conversationId, peopleCount, days, etc.) that match
the @tool annotation on KernelAIToolSet.saveMealPlanState(). This
ensures constrained decoding works correctly.

Previously instructions used snake_case (save_meal_plan_state,
conversation_id, people_count) which caused the model to fail to
trigger constrained decoding, resulting in run_intent calls with
skill names instead of actual tool invocations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model was calling run_intent with skill names instead of using
@tool methods because the injected context block was too verbose
(~100 lines of stage instructions) and overwhelmed the model.

Now buildMealPlanContext returns only the session metadata block plus
a concise directive telling the model to load_skill the appropriate
stage-specific skill. This reduces prompt size and gives the model a
clear, actionable next step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model reliably ignores the [Tool Use] protocol when the prompt is
long, falling back to run_intent with skill names instead of calling
loadSkill. This causes the meal planner to fail because the model
never reaches the stage-specific skill instructions.

Fix: Add a QIR regex pattern for 'plan meals' / 'make a meal plan'
etc. that routes to intentName='load_skill' with skill_name=
'meal_planner_collect'. ChatViewModel intercepts this and injects
a structured systemContext hint telling the model to call
loadSkill(skillName='meal_planner_collect').

Also: reduced buildMealPlanContext to only return session metadata
plus a concise load_skill directive, removing the ~100-line stage
instructions that were overwhelming the model.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model ignores systemContext hints when the prompt is long. The
QIR handler that set systemContext and fell through to E4B was
unreliable — the model called run_js with old skill names instead
of following the hint to call loadSkill.

Fix: When QIR matches load_skill → meal_planner_collect, directly
execute the load_skill skill to get MealPlannerCollectSkill
fullInstructions, then either return as DirectReply (bypassing E4B)
or inject into systemContext for the first E4B turn.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NickMonrad
Copy link
Copy Markdown
Owner

Test results

Logs: kernel_debug_log_0.1.0_20260429_175345_303.txt

You: Plan meals
[Tool: save_meal_plan_state]
Request: "conversation_id":"","status":"collecting_preferences","people_count":"","days":"","dietary_restrictions":"["restriction1", "restriction2"]","protein_preferences":"["protein1", "2"]","high_level_plan":"{}","current_day_index":"0"
Result: Meal planner state saved for conversation .
Status: collecting_preferences
Dietary: ["restriction1", "restriction2"]
Proteins: ["protein1", "2"]
Plan saved.
Current day index: 0
Jandal: How many people are you cooking for, and are there any dietary restrictions?
You: 3 people. Low lactose
Jandal: Righto, 3 people, and low lactose.

How many days are you looking to plan for, and what are your protein preferences (e.g., chicken, fish, vegetarian)?
You: 2 days. Beef and chicken
Jandal: Righto, Nick. So that's 3 people, 2 days, low lactose, and you're keen on beef and chicken.

I'll get the high-level plan sorted for you now.
You: Ok
Jandal: Got it. So, 3 people, 2 days, low lactose, beef and chicken.

I'll start putting together a high-level plan for you can look over.
You: Ok
Jandal: I've got the details: 3 people, 2 days, low lactose, beef and chicken.

I'll generate a high-level plan for you now.
You: Ok do it
[Tool: save_meal_plan_state]
Request: "conversation_id":"conversation_id_placeholder","status":"high_level_plan_ready","people_count":"3","days":"2","dietary_restrictions":"["low lactose"]","protein_preferences":"["beef","chicken"]","high_level_plan":"{}","current_day_index":"0"
Result: Meal planner state saved for conversation conversation_id_placeholder.
Status: high_level_plan_ready
People: 3
Days: 2
Dietary: ["low lactose"]
Proteins: ["beef","chicken"]
Plan saved.
Current day index: 0
Jandal: Righto. I'm putting together a high-level plan for you can look over.

Here is a high-level plan for you, Nick:

Day 1:

  • Dinner: Beef Stir-fry with chicken and vegetables.

Day 2:

  • Dinner: Beef stew with root vegetables.

How does that sound? If you're happy with that, I can start generating the recipes for you.
You: Sounds good
[Tool: save_meal_plan_state]
Request: "conversation_id":"conversationid_placeholder","status":"highlevel_plan_ready","people_count":"3","days":"2","dietary_restrictions":"["low lactose"]","protein_preferences":"["beef","chicken"]","high_level_plan":"{"Day1":"Beef Stir-fry with chicken and vegetables","Day2":"Beef stew with root vegetables"}","current_day_index":"0"
Result: Invalid status: 'highlevel_plan_ready'. Must be one of: collecting_preferences, high_level_plan_ready, generating_recipes, completed.
Jandal: I'm ready to generate the recipes for you, Nick. Which day should I start with?
You: Day 1
Here are the recipes for Day 1:

Beef Stir-fry with chicken and vegetables

Ingredients:

  • 50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    You: Try again
    Jandal:

lokhor and others added 3 commits April 29, 2026 18:26
…anState (#689)

The model ignores systemContext hints and load_skill directives when
the prompt is long. It falls back to run_intent/run_js with learned
patterns instead of following stage instructions.

Fix: After saveMealPlanState persists state, the skill now returns
the next stage skill's fullInstructions as a DirectReply. This
bypasses E4B for stage transitions — the model receives the next
instructions directly without needing to figure out which skill to
load.

Stage transitions:
  collecting_preferences -> Success (model continues collecting)
  high_level_plan_ready -> DirectReply with meal_planner_plan instructions
  generating_recipes -> DirectReply with meal_planner_recipe instructions
  completed -> DirectReply with completion message

Also: Updated SaveMealPlanStateSkillTest for new constructor and
DirectReply returns on stage transitions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…gistry in tests (#689)

The model copies placeholder strings (<conv-id>, <N>) from skill schema
examples and passes them as literal parameter values. This causes
save_meal_plan_state to receive empty/invalid data.

Fix:
1. Reject conversation_id values that start with '<' or contain
   'session block' — these are clearly placeholders.
2. During Stage 1, reject people_count/days that match placeholder
   patterns (contain '<' or 'N>').
3. Remove misleading examples from save_meal_plan_state schema
   (now emptyList) since they are being copied verbatim by the model.
4. Mock meal_planner_plan and meal_planner_recipe skills in the test
   so DirectReply stage transitions work in unit tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…structions (#689)

The model ignores systemContext hints and falls back to calling
run_intent or run_js with skill names (meal_planner_collect, etc.)
instead of using load_skill. Instead of returning errors, intercept
these calls and return the skill's fullInstructions directly in the
tool response so the model can follow the proper flow.

Also remove placeholder examples from all stage skill fullInstructions
to prevent the model from copying literal placeholder strings into
saveMealPlanState calls.

Changes:
- KernelAIToolSet.runIntent: reserved skill names now return full
  instructions instead of error messages
- KernelAIToolSet.runJs: same interception for reserved skill names
- MealPlannerCollectSkill: removed placeholder saveMealPlanState example
- MealPlannerPlanSkill: removed placeholder saveMealPlanState example
- MealPlannerRecipeSkill: removed placeholder saveMealPlanState example
- MealPlannerCompleteSkill: removed placeholder saveMealPlanState example
- ChatViewModel bypass: always DirectReply for meal_planner_collect
- Updated MealPlannerPlanSkillTest assertion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NickMonrad
Copy link
Copy Markdown
Owner

Testing results: it got stuck processing and never finished after several minutes.

You: "plan a meal for 2 people for 3 days, vegetarian
[Tool: run_intent]
Request: {"intent_name":"meal_planner_plan","parameters":{"days":"3","dietary_restrictions":["vegetarian"],"people_count":"2"],"protein_preferences":[]}}
Result: You called run_intent with skill name 'meal_planner_plan' instead of using load_skill.
Here are the full instructions for meal_planner_plan:

meal_planner_plan: Generate a high-level meal plan for the specified days and preferences.

IMPORTANT: Load with load_skill first. Do NOT call run_intent. Only call saveMealPlanState after showing the plan.

SESSION CONTEXT BLOCK: At the start of each turn, the system may inject a [Meal Planner Session] block.
ALWAYS read this first. It contains: status, people_count, days, dietary_restrictions, protein_preferences, high_level_plan.
Treat the session block as ground truth. If your memory conflicts, trust the session block.
Status values and actions:
collecting_preferences -> ask the user for people, restrictions, protein, days (Stage 1).
high_level_plan_ready -> plan already generated; re-show it (context may have been lost).
generating_recipes -> Stage 3 started; do not regenerate the plan.
completed -> done; do not continue.
If status is missing or collecting_preferences, ask: "How many people, dietary restrictions, protein preferences, and how many days?"

GENERATE THE PLAN (when preferences are available):

  1. Show summary: "Here's a 5-day plan for 4 vegetarians with pasta/lentil focus:"
  2. List all days at high-level (one line each): "Day 1: Pasta Carbonara | Day 2: Lentil Soup"
  3. Keep diverse and realistic - vary cuisines and protein sources.
  4. Reflect dietary restrictions and protein preferences from the session block.
  5. Do NOT generate recipes or ingredients yet - just dish names.
  6. Ask: "Ready for the full recipes with cooking steps?"

SAVE STATE (critical):
After showing the plan, call saveMealPlanState with status="high_level_plan_ready".
Use the EXACT parameter names (camelCase). Do NOT use snake_case.
Pass the highLevelPlan as a JSON object mapping day keys to meal names.
Do NOT skip this — without it, context truncation loses the plan.

FORMATTING: Use METRIC / NZ units only (g, kg, ml, l, tsp, tbsp, Celsius, counts). NEVER use lb, oz, Fahrenheit.

Use these instructions to proceed. Do NOT call run_intent again.
Jandal: I've got the instructions for the meal planner. Since you've asked to plan a meal for 2 people for 3 days, vegetarian, I'll put together a high-level plan for you now.

Here's a 3-day plan for 2 vegetarians:

Day 1: Lentil Dahl | Day 2: Vegetable Curry | Day 3: Quinoa Salad

Ready for the full recipes with cooking steps?
You: Yes
Jandal:

Logs
kernel_debug_log_0.1.0_20260429_202632_742.txt

lokhor and others added 5 commits April 29, 2026 20:47
…689)

Three fixes addressing persistent meal planner instruction-following failures:

1. Fix QIR regex to match 'plan a meal' (user said 'plan a meal for 2 people')
   - Added optional 'a' before meal/food/dinner/lunch/breakfast in regex

2. Skip collect stage when user already provided preferences
   - Detect preference keywords (people count, days, dietary restrictions)
   - Load meal_planner_plan directly instead of meal_planner_collect
   - Prevents redundant preference-gathering when user already stated them

3. Fix meal_planner_plan fullInstructions
   - Removed 'Do NOT call run_intent' conflict (model triggered via run_intent)
   - Made saveMealPlanState call explicit and immediate after plan display
   - Added concrete example of highLevelPlan JSON structure

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase A of the meal-planner redesign per docs/research/meal-planner-skill-design.md:

1. MealPlannerStateMachine: explicit state machine with valid transitions
   - States: COLLECTING_PREFERENCES -> PLAN_DRAFT_READY -> PLAN_CONFIRMED ->
     GENERATING_RECIPE -> RECIPE_REVIEW -> WRITING_ARTIFACTS -> COMPLETED
   - Transitions enforced by code, not the LLM

2. MealPlannerCoordinator: deterministic workflow engine
   - Owns flow control, persistence, and stage progression
   - Parses user input for preference keywords (people count, days, dietary)
   - Delegates content generation to stage skills only when needed
   - Thread-safe via Mutex

3. QIR routing: changed from load_skill(meal_planner_collect) to
   start_meal_planner intent

4. KernelAIToolSet: added start_meal_planner to reserved skill names

5. MealPlanSessionRepository: added currentDayIndex parameter to
   updatePreferences()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Inject MealPlannerCoordinator from core:skills (fix import path)
- Add active-meal-planner detection before QIR routing
- Replace load_skill bypass with coordinator.startOrResume()
- Add CoordinatorResult.content property to sealed interface
- Clean up blank lines from previous ast_edit operations
#689)

- Add statusString property to MealPlannerStateMachine.State enum to
  explicitly map states to DB status strings
- Update transitionTo() to use target.statusString instead of
  target.name.lowercase() which produced incorrect statuses
  (e.g. generating_recipe vs generating_recipes)
- Add @provides binding for MealPlannerCoordinator in SkillsModule
  (was missing after MealPlannerSkill was removed)
- Fix corrupted ?: operators in LoadSkillSkill and ChatViewModel
- Remove dead MealPlannerSkill and MealPlannerSkillTest
- Clean up stale references in KernelAIToolSet and LoadSkillSkill
- Add 27 tests in MealPlannerCoordinatorTest covering state transitions,
  regex parsing, and session resume logic
lokhor added 4 commits April 30, 2026 10:08
LoadSkillSkill.execute() was returning a generic 'Instructions loaded'
message without the actual instructions content. The model was told to
'follow them' but could never see the instructions. Now returns the
skill's fullInstructions property in the success response so the model
can actually read and execute them.
ActionsViewModel.executeIntent was routing start_meal_planner through
run_intent → NativeIntentHandler, which doesn't know the intent.
Now routes directly to MealPlannerCoordinator.startOrResume() with
a generated conversation ID, matching the ChatViewModel path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: add minimal meal planner session store to resolve #687 (not full #235)

2 participants