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
1 change: 1 addition & 0 deletions .claude/skills/conductor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ conductor run workflow.yaml -V --input question="Hello" # Full verbose (untrunc
conductor validate workflow.yaml # Validate only
conductor init my-workflow --template simple # Create from template
conductor templates # List templates
conductor stop # Stop background workflow
```

Progress output is shown by default. Use `-V` (verbose) for full prompts and detailed tool call info.
Expand Down
30 changes: 30 additions & 0 deletions .claude/skills/conductor/references/execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,36 @@ conductor run workflow.yaml --web-bg --input question="Hello"

The `--web` flag opens a browser dashboard with a DAG visualization showing live agent status, streaming reasoning/tool calls, and an agent detail panel. The `--web-bg` flag forks a background process and exits immediately. `--web` and `--web-bg` are mutually exclusive.

Background workflows can be stopped with `conductor stop` (see below) or via the stop button in the web dashboard.

### conductor stop

Stop background workflow processes launched with `--web-bg`:

```bash
conductor stop [OPTIONS]
```

| Option | Description |
|--------|-------------|
| `--port PORT` | Stop the workflow running on this specific port |
| `--all` | Stop all background conductor workflows |

With no options, lists running workflows and auto-stops if exactly one is found.

**Examples:**

```bash
# Stop the only running background workflow
conductor stop

# Stop a specific workflow by port
conductor stop --port 8080

# Stop all running background workflows
conductor stop --all
```

### conductor validate

Validate without executing:
Expand Down
10 changes: 8 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ uv run conductor run workflow.yaml --web --input question="What is Python?"
# Run in background (prints dashboard URL and exits)
uv run conductor run workflow.yaml --web-bg --input question="What is Python?"

# Stop a background workflow
uv run conductor stop # auto-stop if one running, list if multiple
uv run conductor stop --port 8080 # stop specific port
uv run conductor stop --all # stop all background workflows

# Validate a workflow
uv run conductor validate examples/simple-qa.yaml
make validate-examples # validate all examples
Expand All @@ -47,10 +52,11 @@ make validate-examples # validate all examples

### Core Package Structure (`src/conductor/`)

- **cli/**: Typer-based CLI with commands `run`, `validate`, `init`, `templates`
- **cli/**: Typer-based CLI with commands `run`, `validate`, `init`, `templates`, `stop`
- `app.py` - Main entry point, defines the Typer application
- `run.py` - Workflow execution command with verbose logging helpers
- `bg_runner.py` - Background process forking for `--web-bg` mode
- `pid.py` - PID file utilities for tracking/stopping background processes

- **config/**: YAML loading and Pydantic schema validation
- `schema.py` - Pydantic models for all workflow YAML structures (WorkflowConfig, AgentDef, ParallelGroup, ForEachDef, etc.)
Expand Down Expand Up @@ -78,7 +84,7 @@ make validate-examples # validate all examples
- `human.py` - Rich terminal UI for human gate interactions

- **web/**: Real-time web dashboard for workflow visualization
- `server.py` - FastAPI + uvicorn server with WebSocket broadcasting and late-joiner state replay
- `server.py` - FastAPI + uvicorn server with WebSocket broadcasting, late-joiner state replay, and `POST /api/stop` endpoint
- `static/index.html` - Single-file Cytoscape.js frontend with DAG graph, agent detail panel, and streaming activity

- **events.py**: Pub/sub event system decoupling workflow execution from rendering (console, web dashboard)
Expand Down
39 changes: 39 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Complete command-line reference for Conductor.
## Table of Contents

- [`conductor run`](#conductor-run)
- [`conductor stop`](#conductor-stop)
- [`conductor validate`](#conductor-validate)
- [`conductor init`](#conductor-init)
- [`conductor templates`](#conductor-templates)
Expand Down Expand Up @@ -95,6 +96,8 @@ The `--web-bg` flag is a convenience shortcut: it forks a background process run

`--web` and `--web-bg` are mutually exclusive.

Background workflows can be stopped with `conductor stop` (see below) or via the stop button in the web dashboard.

#### Automation Mode

```bash
Expand All @@ -120,6 +123,42 @@ Line 2
Line 3"
```

## `conductor stop`

Stop background workflow processes launched with `--web-bg`.

```bash
conductor stop [OPTIONS]
```

### Options

| Option | Description |
|--------|-------------|
| `--port PORT` | Stop the workflow running on this specific port |
| `--all` | Stop all background conductor workflows |

With no options, `conductor stop` lists running background workflows. If exactly one is found, it stops automatically. If multiple are running, it prints the list and asks you to specify `--port`.

### How It Works

When a workflow is launched with `--web-bg`, Conductor writes a PID file to `~/.conductor/runs/` tracking the background process. The `stop` command reads these PID files, sends `SIGTERM` to the process, and cleans up the file. PID files are also automatically cleaned up when a background workflow completes normally.

The web dashboard also has a stop button that cancels the running workflow directly via `POST /api/stop`.

### Examples

```bash
# Stop the only running background workflow
conductor stop

# Stop a specific workflow by port
conductor stop --port 8080

# Stop all running background workflows
conductor stop --all
```

## `conductor validate`

Validate a workflow file without executing it. Checks YAML syntax, schema compliance, and cross-references (agent names, routes, parallel groups).
Expand Down
130 changes: 130 additions & 0 deletions src/conductor/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import contextvars
import os
from enum import Enum
from pathlib import Path
from typing import Annotated, Any
Expand Down Expand Up @@ -690,3 +691,132 @@ def checkpoints(

output_console.print(table)
output_console.print(f"\n[dim]Total: {len(checkpoint_list)} checkpoint(s)[/dim]")


@app.command()
def stop(
port: Annotated[
int | None,
typer.Option(
"--port",
help="Stop the background workflow running on this port.",
),
] = None,
all_workflows: Annotated[
bool,
typer.Option(
"--all",
help="Stop all background conductor workflows.",
),
] = False,
) -> None:
"""Stop background workflow processes launched with --web-bg.

With no arguments, lists running background workflows. If exactly one
is found, stops it automatically. If multiple are found, prints the
list and asks you to specify --port.

\b
Examples:
conductor stop
conductor stop --port 8080
conductor stop --all
"""
from conductor.cli.pid import read_pid_files, remove_pid_file

running = read_pid_files()

if not running:
console.print("[dim]No background workflows are currently running.[/dim]")
return

if all_workflows:
for entry in running:
_stop_process(entry, console)
remove_pid_file(entry["port"])
return

if port is not None:
# Find the entry for the specified port
match = [e for e in running if e["port"] == port]
if not match:
console.print(
f"[bold red]Error:[/bold red] No background workflow found on port {port}."
)
console.print("[dim]Running workflows:[/dim]")
_print_running_list(running, console)
raise typer.Exit(code=1)
_stop_process(match[0], console)
remove_pid_file(port)
return

# No flags: auto-stop if exactly one, otherwise list
if len(running) == 1:
entry = running[0]
_stop_process(entry, console)
remove_pid_file(entry["port"])
else:
console.print(
f"[bold yellow]Multiple background workflows running ({len(running)}).[/bold yellow]"
)
console.print("[dim]Specify --port to stop a specific one, or --all to stop all.[/dim]\n")
_print_running_list(running, console)


def _stop_process(entry: dict, con: Console) -> None:
"""Send SIGTERM (or equivalent) to a background workflow process.

Args:
entry: A PID-file dict with ``pid``, ``port``, ``workflow`` keys.
con: Rich Console for output.
"""
import signal
import sys

pid = entry["pid"]
port = entry["port"]
workflow = Path(entry.get("workflow", "unknown")).stem

try:
if sys.platform == "win32":
os.kill(pid, signal.CTRL_BREAK_EVENT)
else:
os.kill(pid, signal.SIGTERM)
con.print(
f"[green]Stopped[/green] workflow [cyan]'{workflow}'[/cyan] (PID {pid}, port {port})"
)
except ProcessLookupError:
con.print(
f"[dim]Process already exited:[/dim] workflow '{workflow}' (PID {pid}, port {port})"
)
except PermissionError:
con.print(
f"[bold red]Permission denied:[/bold red] could not stop PID {pid}. "
f"Try running with elevated privileges."
)


def _print_running_list(entries: list[dict], con: Console) -> None:
"""Print a table of running background workflows.

Args:
entries: List of PID-file dicts.
con: Rich Console for output.
"""
from rich.table import Table

table = Table(show_lines=False)
table.add_column("Port", style="cyan")
table.add_column("PID", style="yellow")
table.add_column("Workflow", style="white")
table.add_column("Started", style="dim")

for e in entries:
table.add_row(
str(e["port"]),
str(e["pid"]),
Path(e.get("workflow", "unknown")).stem,
e.get("started_at", "?"),
)

con.print(table)
8 changes: 7 additions & 1 deletion src/conductor/cli/bg_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,10 @@ def launch_background(
# Windows: CREATE_NEW_PROCESS_GROUP for detachment
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP

# Set environment variable to signal bg mode to the child
# Set environment variables to signal bg mode to the child
env = os.environ.copy()
env["CONDUCTOR_WEB_BG"] = "1"
env["CONDUCTOR_WEB_PORT"] = str(web_port)
kwargs["env"] = env

try:
Expand All @@ -153,6 +154,11 @@ def launch_background(
f"The background process (PID {proc.pid}) may still be starting."
)

# Write PID file so `conductor stop` can find this process
from conductor.cli.pid import write_pid_file

write_pid_file(proc.pid, web_port, workflow_path)

return f"http://127.0.0.1:{web_port}"


Expand Down
Loading
Loading