Skip to content

Surface failed AI CLI calls from generate_with_memory #101

Open

Description

@twilwa

When the AI CLI exits nonzero or returns empty output, generate_with_memory catches the failure and returns None after retries (

if result.returncode != 0:
error = f"LLM call failed with return code {result.returncode}: {result.stderr}"
self.logger.warning(error)
continue
response = result.stdout.strip()
if response:
break
else:
error = "LLM returned empty response"
self.logger.warning(error)
,
if not response:
raise RuntimeError(f"Failed to get response from LLM after {max_retries} retries: {error}")
,
def generate_with_memory(
self,
memory: Memory,
purpose: str = "general",
max_retries: int = 3,
timeout: Optional[int] = 1800,
metadata: Dict[str, Any] = None,
) -> Optional[str]:
"""Generate from a Memory object by flattening to a single prompt.
Encoder and RPGAgent use Memory to manage multi-turn conversations.
This method flattens the message sequence into a single prompt
string, then invokes the CLI subprocess via ``generate()``.
Differences from ``generate()``:
- Input: Memory object (multiple messages) instead of a single string
- Return: ``Optional[str]`` (None on failure) instead of raising
Args:
memory: Memory instance with SystemMessage / UserMessage /
AssistantMessage entries.
purpose, max_retries, timeout, metadata: Same as ``generate()``.
Returns:
LLM response text, or None if all retries failed.
Raises:
RuntimeError: only when ``self.tool`` is not configured.
Genuine LLM-call failures (subprocess errors, timeouts,
bad responses) are swallowed and surface as ``None``;
missing configuration is an unrecoverable user-action
error and must not be silently masked.
"""
# Eagerly surface the "AI CLI not configured" condition. This is
# not a transient failure that ``None`` should represent — the
# user needs an actionable error. Genuine LLM failures continue
# to be caught below.
if not self.tool or self.tool == _PLACEHOLDER_LITERAL:
raise RuntimeError(
"AI CLI command not configured. Run "
"`cmind init --ai <name>` in this workspace, or set the "
"CMIND_AI_CLI_CMD environment variable."
)
prompt = self._flatten_memory(memory)
try:
return self.generate(
prompt=prompt,
purpose=purpose,
max_retries=max_retries,
timeout=timeout,
metadata=metadata,
)
except RuntimeError:
return None
). parse_solution_output(None) then raises 'NoneType' object has no attribute 'split' each functional-area iteration; the loop returns an empty plan and AssertionError: functional_areas must not be empty aborts before run_encode reaches its dependency-graph build/save steps, so no dependency graph is saved (
def parse_solution_output(output: str) -> str:
"""Extract the content inside ``<solution>...</solution>`` tags.
If the tags are not present the full (stripped) output is returned.
Args:
output: Raw LLM output string.
Returns:
Extracted solution text, stripped of leading/trailing whitespace.
Source: RPG-ZeroRepo/zerorepo/utils/api.py (parse_solution_output)
"""
output = output.split("<solution>", 1)[-1]
output = output.split("</solution>", 1)[0]
return output.strip()
,
for i in range(max_iters):
try:
response = self.llm_client.generate_with_memory(memory)
if i == 0:
memory.add_message(AssistantMessage(response))
parsed_response = parse_solution_output(response)
parsed_response = (
parsed_response.replace("```json", "")
.replace("```", "")
.replace("\n", "")
.replace("\t", "")
)
parsed_result = json.loads(parsed_response)
candidate_results.append(parsed_result)
memory.add_message(AssistantMessage(response))
followup_prompt = (
f"Here is your plan iteration {i + 1}. "
"If you believe there are missing or redundant functional areas, refine the grouping. "
"Return only a corrected JSON."
)
memory.add_message(UserMessage(followup_prompt))
except Exception as e:
self.logger.warning("Iteration %d failed: %s", i + 1, e)
continue
,
else:
self.logger.warning("No valid results from LLM - returning empty dict.")
final_result = {}
return {"candidates": candidate_results, "final_plan": final_result}
,
functional_result = self.plan_functional_areas(
parsed_tree=parsed_tree, max_iters=3
)
functional_areas = functional_result.get("final_plan", [])
assert functional_areas, "functional_areas must not be empty"
,
rpg, feature_tree, skeleton_info = parser.parse_rpg_from_repo(
save_path=output,
max_exclude_votes=max_exclude_votes,
)
,
# Step 3: Save RPG to disk
step_save = traj.add_step("save_rpg", "Save RPG to disk")
traj.start_step(step_save.step_id)
result_data = rpg.to_dict()
# Atomic write of the central pipeline artefact. A killed
# encode used to truncate rpg.json and brick downstream
# stages (skeleton / func_design / code_gen all read it);
# now the previous good rpg.json survives any interrupted
# write.
atomic_write_rpg(output, result_data, indent=2, ensure_ascii=False)
).

This reproduced with the Claude backend on a 7-file Python repo. The workspace had a .cmind/config.toml matching the documented cmind init --ai claude layout; cmind init itself was not run. The /cmind.encode command failed on all three functional-area requests. Please surface the final CLI exit code and a concise stderr summary when generate_with_memory exhausts its retries, rather than returning None and failing later. The open PR at #99 touches llm_client.py, so maintainers could fold this in there.

This issue was AI-generated at the human operator's direction and reviewed by the operator before submission.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions