Skip to content
Draft
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
36 changes: 36 additions & 0 deletions src/smallestai/cli/agent_crew.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,42 @@ async def async_build_logs(build_id: str | None, agent_id_arg: Optional[str] = N
console.print(f"[red]Error streaming build logs: {e}[/red]")
raise typer.Exit(1)

@app.command("cancel")
def cancel_build(
build_id: str = typer.Argument(..., help="Build ID to cancel."),
agent_id: Optional[str] = typer.Option(
None, "--agent-id", help="Agent id (defaults to the linked project agent)."
),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt."),
):
"""Cancel a build that is stuck in QUEUED or BUILDING.

Frees the agent's build queue when a build hangs. A build that has already
finished, failed, or is deploying can't be cancelled.
"""
if not yes and sys.stdin.isatty():
typer.confirm(f"Cancel build {build_id}?", abort=True)
asyncio.run(async_cancel_build(build_id, agent_id))

async def async_cancel_build(build_id: str, agent_id_arg: Optional[str] = None):
agent_id = _resolve_agent_id(agent_id_arg)

credentials = auth_client.get_credentials()
if not credentials or not credentials.get("access_token"):
console.print("[red]Error: You must be logged in first. Run 'smallestai auth login'[/red]")
raise typer.Exit(1)
access_token = credentials["access_token"]

try:
message = await atoms_client.cancel_agent_build(
agent_id=agent_id, build_id=build_id, api_key=access_token
)
except Exception as e:
console.print(f"[red]Could not cancel build {build_id[:12]}...: {e}[/red]")
raise typer.Exit(1)

console.print(f"[bold green]✓ {message}[/bold green] [dim]({build_id[:12]}...)[/dim]")

@app.command()
def doctor(
agent_id: Optional[str] = typer.Option(
Expand Down
35 changes: 35 additions & 0 deletions src/smallestai/cli/lib/atoms.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,41 @@ async def update_agent_build(

return update_build_response.data

async def cancel_agent_build(
self,
agent_id: str,
build_id: str,
api_key: str,
) -> str:
"""Cancel a build stuck in QUEUED/BUILDING. Returns the server's message.

Raises with the API's reason when the build can't be cancelled (e.g. it
is already terminal or deploying, which the backend returns as HTTP 409).
"""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/atoms/v1/sdk/agents/{agent_id}/builds/{build_id}/cancel",
headers={
"Authorization": f"Bearer {api_key}",
},
)

body = None
try:
body = response.json()
except Exception:
pass

if response.status_code >= 400:
errors = body.get("errors") if isinstance(body, dict) else None
if isinstance(errors, list):
errors = "; ".join(str(e) for e in errors)
raise Exception(errors or f"cancel failed (HTTP {response.status_code}).")

data = body.get("data") if isinstance(body, dict) else None
message = data.get("message") if isinstance(data, dict) else None
return message or "Build cancelled"

async def _stream_sse(self, url: str, access_token: str):
"""Open an SSE stream and yield each `data:` frame as a parsed dict.

Expand Down
Loading