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:
parent
f86db9e0d9
commit
614b145f5f
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue