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
34 changes: 33 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ uv run conductor stop --all # stop all background workflows
uv run conductor update # check for and install latest version

# Resume a failed workflow from checkpoint
uv run conductor resume workflow.yaml # resume from latest checkpoint
uv run conductor resume workflow.yaml # resume from latest checkpoint
uv run conductor resume workflow.yaml --web # resume with dashboard
uv run conductor resume workflow.yaml --web-bg # resume with background dashboard
uv run conductor resume workflow.yaml --provider copilot
uv run conductor resume workflow.yaml -m tracker=ado
uv run conductor checkpoints # list available checkpoints

# Validate a workflow
Expand Down Expand Up @@ -162,3 +166,31 @@ All providers (`copilot.py`, `claude.py`) must maintain feature parity. Any chan
- **Reasoning effort**: All providers must accept the unified `reasoning.effort` field (`low` | `medium` | `high` | `xhigh`), translate it to the native API (Copilot `reasoning_effort` on the session; Claude extended `thinking` budget), validate that the selected model supports the requested effort, and raise `ValidationError` with a clear message when it does not. Any reasoning/thinking content the model returns must be surfaced via `agent_reasoning` events so the dashboard, JSONL logger, and console subscriber render it consistently.

When modifying any provider, check all other providers for the same change. The dashboard, JSONL logger, console subscriber, and workflow engine all depend on consistent behavior across providers.

### Run / Resume Parity

The `run` and `resume` commands must accept the same flags wherever a flag is meaningful for a resumed run. When adding a new flag to `run`, add it to `resume` too unless there's a specific reason it cannot apply.

Flags that **must** be mirrored on both:

- `--provider` / `-p` — runtime provider override
- `--metadata` / `-m` — CLI metadata merged on top of YAML metadata
- `--skip-gates` — auto-select first option at human gates
- `--log-file` / `-l` — debug log file path (`auto` or explicit)
- `--no-interactive` — disable Esc-to-pause keyboard listener
- `--web` — start the real-time web dashboard
- `--web-port` — dashboard port (0 = auto-select)
- `--web-bg` — fork a detached process running the workflow + dashboard

Flags intentionally **not** mirrored on `resume` (and why):

- `--input` / `-i` — workflow inputs are restored from the checkpoint context; supplying them at resume would conflict.
- `--workspace-instructions`, `--instructions` — the `instructions_preamble` is persisted in the checkpoint and restored verbatim; re-supplying would be ambiguous.
- `--dry-run` — resume executes from a saved point and is incompatible with planning-only output.

Implementation parity rules:

- The async helpers (`run_workflow_async` and `resume_workflow_async` in `cli/run.py`) must wire up the same event emitter, JSONL event log subscriber, console event subscriber, and `WebDashboard` lifecycle.
- The `WorkflowEngine` constructor receives the same kwargs in both paths (`event_emitter`, `web_dashboard`, `run_context`, `interrupt_event`, `keyboard_listener`, `instructions_preamble`).
- Background-process forking lives in `cli/bg_runner.py`. `run --web-bg` calls `launch_background()` and `resume --web-bg` calls `launch_background_resume()`. Both must forward equivalent options and write a PID file via `cli/pid.py`.
- Note: on resume, the dashboard only shows events from the resumed agent forward — events from agents that completed before the checkpoint were emitted in the original process and are not replayed.
98 changes: 97 additions & 1 deletion src/conductor/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,25 @@ def resume(
help="Path to a specific checkpoint file to resume from.",
),
] = None,
provider: Annotated[
str | None,
typer.Option(
"--provider",
"-p",
help="Override the provider specified in the workflow (e.g., 'copilot').",
),
] = None,
raw_metadata: Annotated[
list[str] | None,
typer.Option(
"--metadata",
"-m",
help=(
"Workflow metadata in key=value format. "
"Merged on top of YAML metadata. Can be repeated."
),
),
] = None,
skip_gates: Annotated[
bool,
typer.Option(
Expand All @@ -713,6 +732,31 @@ def resume(
help="Disable interactive interrupt capability (Esc to pause).",
),
] = False,
web: Annotated[
bool,
typer.Option(
"--web",
help="Start a real-time web dashboard for workflow visualization.",
),
] = False,
web_port: Annotated[
int,
typer.Option(
"--web-port",
help="Port for the web dashboard (0 = auto-select).",
),
] = 0,
web_bg: Annotated[
bool,
typer.Option(
"--web-bg",
help=(
"Run resumed workflow + dashboard in a background process. "
"Prints the dashboard URL and exits immediately. "
"Does not require --web."
),
),
] = False,
) -> None:
"""Resume a workflow from a checkpoint after failure.

Expand All @@ -723,18 +767,32 @@ def resume(
Either provide a workflow file (to find the latest checkpoint) or
use --from to specify a checkpoint file directly.

Note: when running with --web or --web-bg, the dashboard only shows
events from the resumed agent forward. Agent runs that completed
before the checkpoint were emitted in the original process and are
not replayed.

\b
Examples:
conductor resume workflow.yaml
conductor resume --from /tmp/conductor/checkpoints/my-workflow-20260224-153000.json
conductor resume workflow.yaml --skip-gates
conductor resume workflow.yaml --log-file auto
conductor resume workflow.yaml --no-interactive
conductor resume workflow.yaml --provider copilot
conductor resume workflow.yaml --metadata tracker=ado -m work_item_id=1814
conductor resume workflow.yaml --web
conductor resume workflow.yaml --web --web-port 8080
conductor resume workflow.yaml --web-bg
"""
import asyncio
import json

from conductor.cli.run import generate_log_path, resume_workflow_async
from conductor.cli.run import (
generate_log_path,
parse_metadata_flags,
resume_workflow_async,
)

# Validate arguments
if workflow is None and from_checkpoint is None:
Expand All @@ -748,6 +806,10 @@ def resume(
)
raise typer.Exit(code=1)

# Validate mutually exclusive flags
if web and web_bg:
raise typer.BadParameter("--web and --web-bg are mutually exclusive")

# Resolve workflow ref if provided
resolved_workflow: Path | None = None
if workflow is not None:
Expand Down Expand Up @@ -789,6 +851,11 @@ def resume(
)
raise typer.Exit(code=1)

# Parse --metadata key=value flags (no type coercion)
cli_metadata: dict[str, str] = {}
if raw_metadata:
cli_metadata.update(parse_metadata_flags(raw_metadata))

# Resolve log file path
resolved_log_file: Path | None = None
if log_file is not None:
Expand All @@ -798,14 +865,43 @@ def resume(
else:
resolved_log_file = Path(log_file)

# Handle --web-bg: fork a background process and exit immediately
if web_bg:
from conductor.cli.bg_runner import launch_background_resume

try:
url = launch_background_resume(
workflow_path=resolved_workflow,
checkpoint_path=resolved_checkpoint,
provider_override=provider,
skip_gates=skip_gates,
log_file=resolved_log_file,
web_port=web_port,
metadata=cli_metadata,
)
console.print(f"[bold cyan]Dashboard:[/bold cyan] {url}")
console.print(
"[dim]Resumed workflow running in background. Dashboard auto-shuts down after "
"workflow completes and all clients disconnect.[/dim]"
)
except Exception as e:
print_error(e)
raise typer.Exit(code=1) from None
return

try:
result = asyncio.run(
resume_workflow_async(
workflow_path=resolved_workflow,
checkpoint_path=resolved_checkpoint,
provider_override=provider,
skip_gates=skip_gates,
log_file=resolved_log_file,
no_interactive=no_interactive,
web=web,
web_port=web_port,
web_bg=web_bg,
metadata=cli_metadata,
)
)

Expand Down
Loading
Loading