Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions modules/tool-recipes/amplifier_module_tool_recipes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ def description(self) -> str:
- approvals: List pending approvals across sessions
- approve: Approve a stage to continue execution
- deny: Deny a stage to stop execution
- cancel: Cancel a running recipe session (graceful or immediate)

Example:
Execute recipe: {{"operation": "execute", "recipe_path": "@recipes:examples/code-review.yaml", "context": {{"file_path": "src/auth.py"}}}}
Expand All @@ -250,7 +251,8 @@ def description(self) -> str:
Validate recipe: {{"operation": "validate", "recipe_path": "@recipes:examples/my-recipe.yaml"}}
List approvals: {{"operation": "approvals"}}
Approve stage: {{"operation": "approve", "session_id": "...", "stage_name": "planning"}}
Deny stage: {{"operation": "deny", "session_id": "...", "stage_name": "planning", "reason": "needs revision"}}"""
Deny stage: {{"operation": "deny", "session_id": "...", "stage_name": "planning", "reason": "needs revision"}}
Cancel recipe: {{"operation": "cancel", "session_id": "...", "immediate": false}}"""

@property
def input_schema(self) -> dict:
Expand All @@ -267,6 +269,7 @@ def input_schema(self) -> dict:
"approvals",
"approve",
"deny",
"cancel",
],
"description": "Operation to perform",
},
Expand All @@ -280,7 +283,7 @@ def input_schema(self) -> dict:
},
"session_id": {
"type": "string",
"description": "Session ID (required for 'resume', 'approve', 'deny' operations)",
"description": "Session ID (required for 'resume', 'approve', 'deny', 'cancel' operations)",
},
"stage_name": {
"type": "string",
Expand All @@ -290,6 +293,10 @@ def input_schema(self) -> dict:
"type": "string",
"description": "Reason for denial (optional for 'deny' operation)",
},
"immediate": {
"type": "boolean",
"description": "If true, request immediate cancellation (don't wait for current step). For 'cancel' operation.",
},
},
"required": ["operation"],
}
Expand Down Expand Up @@ -321,6 +328,8 @@ async def execute(self, input: dict[str, Any]) -> ToolResult:
return await self._approve_stage(input)
if operation == "deny":
return await self._deny_stage(input)
if operation == "cancel":
return await self._cancel_recipe(input)
return ToolResult(
success=False,
error={"message": f"Unknown operation: {operation}"},
Expand Down Expand Up @@ -750,3 +759,79 @@ async def _deny_stage(self, input: dict[str, Any]) -> ToolResult:
success=False,
error={"message": f"Failed to deny stage: {str(e)}"},
)

async def _cancel_recipe(self, input: dict[str, Any]) -> ToolResult:
"""Cancel a running recipe session.

First cancellation request triggers graceful cancellation (complete current step).
Second request (or immediate=True) triggers immediate cancellation.
Cancelled sessions can be resumed later.
"""
session_id = input.get("session_id")
immediate = input.get("immediate", False)

if not session_id:
return ToolResult(
success=False,
error={"message": "session_id is required for cancel operation"},
)

project_path = Path.cwd()

# Verify session exists
if not self.session_manager.session_exists(session_id, project_path):
return ToolResult(
success=False,
error={"message": f"Session not found: {session_id}"},
)

# Check current cancellation status
from .session import CancellationStatus

current_status = self.session_manager.get_cancellation_status(
session_id, project_path
)

if current_status == CancellationStatus.CANCELLED:
return ToolResult(
success=False,
error={
"message": f"Session already cancelled: {session_id}. Use 'resume' to restart.",
},
)

# Request cancellation
success, message = self.session_manager.request_cancellation(
session_id, project_path, immediate=immediate
)

if not success:
return ToolResult(
success=False,
error={"message": message},
)

# Determine the cancellation level
new_status = self.session_manager.get_cancellation_status(
session_id, project_path
)
level = (
"immediate" if new_status == CancellationStatus.IMMEDIATE else "graceful"
)

return ToolResult(
success=True,
output={
"status": "cancellation_requested",
"session_id": session_id,
"level": level,
"message": message,
"next_steps": (
"Recipe will stop immediately."
if level == "immediate"
else "Recipe will stop after current step completes. "
"Send another cancel request (or use immediate=true) for immediate cancellation."
),
"resume_info": "Use 'resume' operation to restart the recipe from where it stopped.",
},
)
Loading