Skip to content

Commit cbe146f

Browse files
committed
🔧 fix(core): fix orchestration loops, over-correction, and redundant LLM calls
Root cause analysis comparing against Claude Code revealed Meeseeks made 12-14 LLM calls per 3-step task (vs ~3 for CC). Each extra call introduced drift, plan inflation, and loop risk. Prompts: - action-planner: enforce 1-2 steps for simple tasks, max 5 total - plan-updater: NEVER add new steps, return empty when goal met - step-executor: prefer "respond" over "tool" when uncertain - reflection: default to "ok" unless clear failure (error/empty/contradiction) Hard guards: - Cap PlanUpdater output (never more steps out than in) - Skip PlanUpdater when only 1 step remains (saves 1 LLM call) - Pass budget context (steps_run/max_steps) to PlanUpdater - Reduce max_steps multiplier from 5 to 3 (default 9 instead of 15) Redundant LLM call elimination: - Remove ToolSelector from act-mode flow (Planner already filters by intent) - Make reflection opt-in per tool via metadata.reflect (shell/edit only) Cleanup: - Remove dead _should_replan() and _build_revised_query() methods - Add .mcp.json to .gitignore (contains auth tokens)
1 parent 18a6e86 commit cbe146f

11 files changed

Lines changed: 67 additions & 90 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Ignore Repo to Prompt
22
repo-to-prompt.codemod.js
33

4+
# MCP config (contains auth tokens)
5+
.mcp.json
6+
47
# Byte-compiled / optimized / DLL files
58
__pycache__/
69
*.py[cod]

packages/meeseeks_core/src/meeseeks_core/action_runner.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,10 @@ def _execute_step(self, action_step: ActionStep) -> StepOutcome:
199199
content = "" if action_result is None else str(action_result)
200200
reflection = None
201201
if self._reflector is not None:
202-
reflection = self._reflector.reflect(action_step, content)
202+
spec = self._tool_registry.get_spec(action_step.tool_id)
203+
should_reflect = spec is not None and spec.metadata.get("reflect", False)
204+
if should_reflect:
205+
reflection = self._reflector.reflect(action_step, content)
203206
return StepOutcome(content=str(content), reflection=reflection)
204207

205208
def _handle_tool_error(

packages/meeseeks_core/src/meeseeks_core/orchestrator.py

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
PlanUpdater,
2424
ResponseSynthesizer,
2525
StepExecutor,
26-
ToolSelector,
2726
)
2827
from meeseeks_core.reflection import StepReflector
2928
from meeseeks_core.session_store import SessionStore
@@ -59,7 +58,6 @@ def __init__(
5958
self._hook_manager = hook_manager or default_hook_manager()
6059
self._context_builder = ContextBuilder(self._session_store)
6160
self._planner = Planner(self._tool_registry)
62-
self._tool_selector = ToolSelector(self._tool_registry)
6361
self._step_executor = StepExecutor(self._tool_registry)
6462
self._plan_updater = PlanUpdater(self._tool_registry)
6563
self._synthesizer = ResponseSynthesizer(self._tool_registry)
@@ -146,16 +144,8 @@ def _run_with_session_context(
146144
else self._tool_registry.list_specs_for_mode("act")
147145
)
148146
if plan is None:
149-
if resolved_mode != "plan":
150-
selection = self._tool_selector.select(
151-
user_query,
152-
self._model_name,
153-
tool_specs=tool_specs,
154-
context=context,
155-
)
156-
if selection.tool_required and selection.tool_ids:
157-
selected_ids = self._expand_tool_ids(set(selection.tool_ids), tool_specs)
158-
tool_specs = [spec for spec in tool_specs if spec.tool_id in selected_ids]
147+
# Tool filtering is handled by Planner._filter_specs_by_intent()
148+
# No separate ToolSelector LLM call needed in act mode
159149
plan = self._planner.generate(
160150
user_query,
161151
self._model_name,
@@ -183,7 +173,7 @@ def _run_with_session_context(
183173
state.done = True
184174
state.done_reason = "planned"
185175
else:
186-
max_steps = max(0, max_iters) * 5
176+
max_steps = max(0, max_iters) * 3
187177
steps_run = 0
188178
allowed_tool_ids = {spec.tool_id for spec in tool_specs}
189179
while remaining_steps and steps_run < max_steps:
@@ -250,15 +240,21 @@ def _run_with_session_context(
250240
state.done = True
251241
state.done_reason = "canceled"
252242
break
253-
if remaining_steps:
243+
if remaining_steps and len(remaining_steps) > 1:
244+
old_count = len(remaining_steps)
254245
remaining_steps = self._plan_updater.update(
255246
user_query,
256247
self._model_name,
257248
completed_step=current_step,
258249
last_result=tool_outputs[-1] if tool_outputs else None,
259250
remaining_steps=remaining_steps,
260251
context=context,
252+
steps_run=steps_run,
253+
max_steps=max_steps,
261254
)
255+
# Hard guard: never allow plan inflation
256+
if len(remaining_steps) > old_count:
257+
remaining_steps = remaining_steps[:old_count]
262258
state.plan = completed_steps + remaining_steps
263259
self._append_action_plan(session_id, state.plan)
264260

@@ -541,17 +537,6 @@ def _should_synthesize_response(task_queue: TaskQueue) -> bool:
541537
return True
542538
return bool(Orchestrator._collect_tool_outputs(task_queue))
543539

544-
@staticmethod
545-
def _build_revised_query(user_query: str, task_queue: TaskQueue) -> str:
546-
failure_note = (
547-
f"Last tool failure: {task_queue.last_error}\n" if task_queue.last_error else ""
548-
)
549-
return (
550-
f"{user_query}\n\nPrevious tool results:\n{task_queue.task_result or ''}\n"
551-
f"{failure_note}"
552-
"Please revise the action plan to resolve remaining tasks."
553-
)
554-
555540
@staticmethod
556541
def _resolve_mode(user_query: str, mode: str | None) -> str:
557542
if mode in {"plan", "act"}:
@@ -569,17 +554,6 @@ def _resolve_mode(user_query: str, mode: str | None) -> str:
569554
return "plan"
570555
return "act"
571556

572-
@staticmethod
573-
def _should_replan(task_queue: TaskQueue, iteration: int, max_iters: int, *, mode: str) -> bool:
574-
if iteration >= max_iters - 1:
575-
return False
576-
if mode == "plan":
577-
return False
578-
if task_queue.last_error:
579-
lowered = task_queue.last_error.lower()
580-
if "permission denied" in lowered or "tool not allowed" in lowered:
581-
return False
582-
return True
583557

584558

585559
__all__ = ["Orchestrator"]

packages/meeseeks_core/src/meeseeks_core/planning.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,25 +526,36 @@ def update(
526526
last_result: str | None,
527527
remaining_steps: list[PlanStep],
528528
context: ContextSnapshot | None = None,
529+
steps_run: int = 0,
530+
max_steps: int = 0,
529531
) -> list[PlanStep]:
530532
"""Return updated remaining steps."""
531533
parser = PydanticOutputParser(pydantic_object=PlanUpdate) # type: ignore[type-var]
532534
system_prompt = get_system_prompt("plan-updater")
533535
remaining_lines = [f"- {step.title}: {step.description}" for step in remaining_steps]
534536
remaining_text = "\n".join(remaining_lines) or "(none)"
537+
budget_note = ""
538+
if max_steps > 0:
539+
budget_note = (
540+
f"Steps completed so far: {steps_run} of {max_steps} maximum.\n"
541+
"If most steps are used, prioritize completing the goal over thoroughness.\n\n"
542+
)
535543
prompt = ChatPromptTemplate(
536544
messages=[
537545
SystemMessage(content=system_prompt),
538546
HumanMessagePromptTemplate.from_template(
539547
"User request:\n{user_query}\n\n"
540548
"Completed step:\n- {title}\n- {description}\n\n"
541549
"Latest result:\n{result}\n\n"
550+
"{budget_note}"
542551
"Remaining steps:\n{remaining}\n\n"
543552
"{format_instructions}"
544553
),
545554
],
546555
partial_variables={"format_instructions": parser.get_format_instructions()},
547-
input_variables=["user_query", "title", "description", "result", "remaining"],
556+
input_variables=[
557+
"user_query", "title", "description", "result", "budget_note", "remaining",
558+
],
548559
)
549560
model = build_chat_model(
550561
model_name=model_name,
@@ -557,6 +568,7 @@ def update(
557568
"title": completed_step.title,
558569
"description": completed_step.description,
559570
"result": last_result or "",
571+
"budget_note": budget_note,
560572
"remaining": remaining_text,
561573
}
562574
)

packages/meeseeks_core/src/meeseeks_core/prompts/action-planner.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ You are Meeseeks, a task-completing agent running on a configured model. Create
33
- `description` (one paragraph describing the step)
44

55
Guidelines:
6+
- Prefer the FEWEST steps possible. A simple task MUST have 1-2 steps. Never generate more than 5 steps total.
7+
- If a task can be done in one tool call, create exactly one step.
8+
- Do NOT create separate steps for "verify" or "validate" unless explicitly requested.
69
- Keep steps crisp and relevant.
7-
- Include steps even if no tool is needed (e.g., Answer directly).
10+
- Include steps even if no tool is needed (e.g., "Answer directly").
811
- Do not output tool calls or tool arguments; tool selection happens separately.
9-
- If the user mentions local files, current directory, or \"pwd\", include a step to inspect local files.
12+
- If the user mentions local files, current directory, or "pwd", include a step to inspect local files.
1013
- If the request needs external data, include a step to retrieve it.
1114
- Examples in the prompt are illustrative only; the actual user request begins at the final user query message.
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
You are Meeseeks. Decide whether to update the remaining plan steps based on the latest result.
1+
You are Meeseeks. Review the remaining plan steps after the latest result.
22

33
Return a PlanUpdate JSON object with:
44
- steps: the updated remaining steps (title + description)
55

66
Rules:
7-
- Only include remaining steps (do not repeat completed steps).
8-
- If no changes are needed, return the same steps unchanged.
9-
- Keep steps concise and in a logical order.
7+
- NEVER add new steps. You may only KEEP or REMOVE existing remaining steps.
8+
- If the latest result already achieves the user's goal, return an EMPTY steps list.
9+
- If a remaining step is redundant given the latest result, remove it.
10+
- When in doubt, remove rather than keep.
11+
- Do not repeat completed steps.

packages/meeseeks_core/src/meeseeks_core/prompts/step-executor.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ Rules:
1010
- Use only tool IDs listed under "Allowed tools".
1111
- If a tool has a schema, args must match it.
1212
- Prefer calling a tool when the step requires external information, file/system access, or verification.
13-
- Respond directly only if you can answer from the given context or you need a single clarifying question from the user.
13+
- If the plan step's purpose is already satisfied by prior context or the user's query can be answered directly, choose "respond".
14+
- Prefer "respond" over "tool" when uncertain.

packages/meeseeks_core/src/meeseeks_core/reflection.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,14 @@ def reflect(self, action_step: ActionStep, result_text: str) -> StepReflection |
5757
SystemMessage(
5858
content=(
5959
"Reflect on whether the tool result satisfies the step objective. "
60-
"Return status 'ok' if complete, 'retry' if the step should be "
61-
"re-executed, or 'revise' if the tool input needs adjustment."
60+
"Return status 'ok' unless there is a CLEAR failure: "
61+
"the tool returned an explicit error message, "
62+
"the tool produced completely empty output when output was expected, "
63+
"or the result directly contradicts the objective. "
64+
"In all other cases, return 'ok'. "
65+
"Partial or imperfect results are acceptable. "
66+
"Do NOT return 'retry' or 'revise' for cosmetic issues "
67+
"or minor imperfections."
6268
)
6369
),
6470
HumanMessagePromptTemplate.from_template(

packages/meeseeks_core/src/meeseeks_core/tool_registry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ def _default_registry() -> ToolRegistry:
181181
"AiderEditBlockTool",
182182
),
183183
prompt_path="tools/aider-edit-blocks",
184+
metadata={"reflect": True},
184185
)
185186
)
186187
registry.register(
@@ -219,6 +220,7 @@ def _default_registry() -> ToolRegistry:
219220
"AiderShellTool",
220221
),
221222
prompt_path="tools/aider-shell",
223+
metadata={"reflect": True},
222224
)
223225
)
224226
return registry
@@ -262,6 +264,7 @@ def _built_in_manifest_entries() -> list[dict[str, object]]:
262264
"kind": "local",
263265
"enabled": True,
264266
"prompt": "tools/aider-edit-blocks",
267+
"reflect": True,
265268
},
266269
{
267270
"tool_id": "aider_read_file_tool",
@@ -294,6 +297,7 @@ def _built_in_manifest_entries() -> list[dict[str, object]]:
294297
"kind": "local",
295298
"enabled": True,
296299
"prompt": "tools/aider-shell",
300+
"reflect": True,
297301
},
298302
]
299303
if not ha_status.enabled and ha_status.reason:

tests/test_action_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ class DummyReflection:
387387
name="Dummy",
388388
description="Dummy tool",
389389
factory=lambda: DummyTool(),
390+
metadata={"reflect": True},
390391
)
391392
)
392393
runner = ActionPlanRunner(

0 commit comments

Comments
 (0)