Improve error messages for debugging

Code step errors:
- Show line numbers with context (line before/after)
- Display available variables for debugging
- Include step number in error message

YAML loading errors:
- Show line and column number for syntax errors
- Display the problematic line with arrow pointer
- Show the specific YAML problem description

Nested tool errors:
- Track call stack through tool chain
- Display full call path when nested tool fails
- Show step numbers at each level

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-01-13 04:50:19 -04:00
parent f86db9e0d9
commit 614b145f5f
3 changed files with 111 additions and 10 deletions

View File

@ -469,6 +469,26 @@ exec {python_path} -m cmdforge.runner {owner}/{name} "$@"
data = self._convert_legacy_format(data)
return Tool.from_dict(data)
except yaml.YAMLError as e:
logger.warning(f"YAML error in {config_path}: {e}")
if self.verbose:
print(f"Error loading tool from '{config_path}': YAML syntax error", file=sys.stderr)
if hasattr(e, 'problem_mark') and e.problem_mark:
mark = e.problem_mark
print(f" Line {mark.line + 1}, column {mark.column + 1}", file=sys.stderr)
try:
lines = config_path.read_text().split('\n')
if mark.line < len(lines):
print(file=sys.stderr)
if mark.line > 0:
print(f" {mark.line}: {lines[mark.line - 1]}", file=sys.stderr)
print(f" > {mark.line + 1}: {lines[mark.line]}", file=sys.stderr)
print(f" {' ' * (mark.column + 4)}^", file=sys.stderr)
except Exception:
pass
if hasattr(e, 'problem') and e.problem:
print(f"\n Problem: {e.problem}", file=sys.stderr)
return None
except Exception as e:
logger.warning(f"Error loading tool from {config_path}: {e}")
if self.verbose:

View File

@ -129,13 +129,14 @@ def execute_prompt_step(step: PromptStep, variables: dict, provider_override: st
return result.text, True
def execute_code_step(step: CodeStep, variables: dict) -> tuple[dict, bool]:
def execute_code_step(step: CodeStep, variables: dict, step_num: int = 0) -> tuple[dict, bool]:
"""
Execute a code step.
Args:
step: The code step to execute
variables: Current variable values (available in code)
step_num: Step number for error reporting
Returns:
Tuple of (output_vars_dict, success)
@ -159,17 +160,56 @@ def execute_code_step(step: CodeStep, variables: dict) -> tuple[dict, bool]:
return outputs, True
except Exception as e:
print(f"Error in code step: {e}", file=sys.stderr)
import traceback
code_lines = code.split('\n')
# Extract line number from traceback
error_line = None
tb = traceback.extract_tb(e.__traceback__)
for frame in tb:
if frame.filename == '<string>': # exec'd code
error_line = frame.lineno
break
print(f"Error in code step (step {step_num}):", file=sys.stderr)
print(f" {type(e).__name__}: {e}", file=sys.stderr)
if error_line and 0 < error_line <= len(code_lines):
print(file=sys.stderr)
# Show context: line before, error line, line after
start = max(0, error_line - 2)
end = min(len(code_lines), error_line + 1)
for i in range(start, end):
marker = ">>>" if i == error_line - 1 else " "
print(f" {marker} {i+1}: {code_lines[i]}", file=sys.stderr)
print(file=sys.stderr)
print(f" Available variables: {list(variables.keys())}", file=sys.stderr)
return {}, False
def _print_call_stack(call_stack: list, error_msg: str) -> None:
"""Print the tool call stack for error reporting."""
if len(call_stack) > 1:
print("Error in tool chain:", file=sys.stderr)
for i, (name, step_num) in enumerate(call_stack):
indent = " " * i
arrow = "-> " if i > 0 else ""
step_info = f" (step {step_num})" if step_num else ""
print(f"{indent}{arrow}{name}{step_info}", file=sys.stderr)
print(f"{' ' * len(call_stack)}{error_msg}", file=sys.stderr)
else:
print(f"Error: {error_msg}", file=sys.stderr)
def execute_tool_step(
step: ToolStep,
variables: dict,
depth: int = 0,
provider_override: Optional[str] = None,
dry_run: bool = False,
verbose: bool = False
verbose: bool = False,
call_stack: Optional[list] = None
) -> tuple[str, bool]:
"""
Execute a tool step by calling another tool.
@ -181,12 +221,16 @@ def execute_tool_step(
provider_override: Override provider for nested calls
dry_run: Just show what would happen
verbose: Show debug info
call_stack: List of (tool_name, step_num) tuples for error reporting
Returns:
Tuple of (output_value, success)
"""
if call_stack is None:
call_stack = []
if depth >= MAX_TOOL_DEPTH:
print(f"Error: Maximum tool nesting depth ({MAX_TOOL_DEPTH}) exceeded", file=sys.stderr)
_print_call_stack(call_stack, f"Maximum tool nesting depth ({MAX_TOOL_DEPTH}) exceeded")
return "", False
# Resolve the tool reference
@ -194,7 +238,7 @@ def execute_tool_step(
resolved = resolve_tool(step.tool)
nested_tool = resolved.tool
except ToolNotFoundError as e:
print(f"Error: Tool '{step.tool}' not found", file=sys.stderr)
_print_call_stack(call_stack, f"Tool '{step.tool}' not found")
print(f"Hint: Install it with: cmdforge install {step.tool}", file=sys.stderr)
return "", False
@ -226,7 +270,8 @@ def execute_tool_step(
dry_run=dry_run,
show_prompt=False,
verbose=verbose,
_depth=depth + 1 # Track nesting depth
_depth=depth + 1,
_call_stack=call_stack
)
return output, exit_code == 0
@ -240,7 +285,8 @@ def run_tool(
dry_run: bool = False,
show_prompt: bool = False,
verbose: bool = False,
_depth: int = 0 # Internal: tracks nesting depth for tool steps
_depth: int = 0,
_call_stack: Optional[list] = None
) -> tuple[str, int]:
"""
Execute a tool.
@ -254,10 +300,16 @@ def run_tool(
show_prompt: Show prompts in addition to output
verbose: Show debug info
_depth: Internal recursion depth tracker
_call_stack: Internal call stack for error reporting
Returns:
Tuple of (output_text, exit_code)
"""
# Initialize call stack
if _call_stack is None:
_call_stack = []
current_stack = _call_stack + [(tool.name, None)]
# Check dependencies on first level only (not for nested calls)
if _depth == 0 and (tool.dependencies or any(isinstance(s, ToolStep) for s in tool.steps)):
missing = check_dependencies(tool)
@ -324,7 +376,7 @@ def run_tool(
for var in [v.strip() for v in step.output_var.split(',')]:
variables[var] = "[DRY RUN - would execute code]"
else:
outputs, success = execute_code_step(step, variables)
outputs, success = execute_code_step(step, variables, step_num=i+1)
if not success:
return "", 1
# Merge all output vars into variables
@ -337,13 +389,16 @@ def run_tool(
print(f" Args: {step.args}", file=sys.stderr)
print("=== END TOOL ===", file=sys.stderr)
# Update call stack with current step number
step_stack = _call_stack + [(tool.name, i + 1)]
output, success = execute_tool_step(
step,
variables,
depth=_depth,
provider_override=provider_override,
dry_run=dry_run,
verbose=verbose
verbose=verbose,
call_stack=step_stack
)
if not success:
return "", 3

View File

@ -316,8 +316,34 @@ def load_tool(name: str) -> Optional[Tool]:
}
return Tool.from_dict(data)
except yaml.YAMLError as e:
import sys
print(f"Error loading tool '{name}': YAML syntax error", file=sys.stderr)
if hasattr(e, 'problem_mark') and e.problem_mark:
mark = e.problem_mark
print(f" Line {mark.line + 1}, column {mark.column + 1}", file=sys.stderr)
# Show the problematic line with context
try:
lines = config_path.read_text().split('\n')
if mark.line < len(lines):
print(file=sys.stderr)
# Show line before for context
if mark.line > 0:
print(f" {mark.line}: {lines[mark.line - 1]}", file=sys.stderr)
print(f" > {mark.line + 1}: {lines[mark.line]}", file=sys.stderr)
print(f" {' ' * (mark.column + 4)}^", file=sys.stderr)
except Exception:
pass
if hasattr(e, 'problem') and e.problem:
print(f"\n Problem: {e.problem}", file=sys.stderr)
return None
except KeyError as e:
import sys
print(f"Error loading tool '{name}': Missing required field {e}", file=sys.stderr)
return None
except Exception as e:
print(f"Error loading tool {name}: {e}")
import sys
print(f"Error loading tool '{name}': {e}", file=sys.stderr)
return None