Skip to content
Open
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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ step-by-step checklist.
- `loader.py` - YAML parsing with environment variable resolution (${VAR:-default}) and `!file` tag support
- `validator.py` - Cross-reference validation (agent names, routes, parallel groups)

- **skills/**: Skill registry and loader (opt-in, bundled skill content)
- `registry.py` - Resolves built-in skill names to on-disk directories (probes editable-install + wheel-install layouts)
- `loader.py` - Reads `SKILL.md` + `references/*.md` for providers that require eager preamble injection; wraps each skill in `<skill name="...">` tags inside a `<skills>` envelope
- Built-in skills live under `plugins/conductor/skills/<name>/` (bundled into the wheel via hatchling `force-include`)

- **engine/**: Workflow execution orchestration
- `workflow.py` - Main `WorkflowEngine` class that orchestrates agent execution, parallel groups, for-each groups, and routing
- `context.py` - `WorkflowContext` manages accumulated agent outputs with three modes: accumulate, last_only, explicit
Expand Down Expand Up @@ -152,6 +157,7 @@ step-by-step checklist.
- **Set step typing**: `output_type` defaults to `auto` (safe YAML parse with `_to_json_safe` normalisation — `datetime`/`date`/`time` → ISO 8601, non-string dict keys and other non-JSON-safe values raise `ExecutionError`). Explicit `string`/`number`/`integer`/`boolean`/`list`/`dict` only valid on single `value:`. `WorkflowContext.store` accepts any JSON-safe value (scalars/lists from `set` steps in addition to the dicts produced by LLM / script / gate / parallel-group outputs); `_add_agent_input` returns the scalar verbatim for `step.output` and raises a clear `KeyError` for `step.output.field` shorthand on non-dict outputs.
- **Reasoning effort**: `runtime.default_reasoning_effort` sets a workflow-wide default; per-agent `reasoning.effort` overrides it. Allowed values: `low`, `medium`, `high`, `xhigh`. Each provider translates the unified value to its native API (Copilot: `reasoning_effort` on the session, validated against the model's `supported_reasoning_efforts`; Claude: extended thinking with budget mapping low=2048, medium=8192, high=16384, xhigh=32768 tokens, with `temperature` coerced to 1.0 and `max_tokens` bumped to fit the budget). See `examples/reasoning-effort.yaml`.
- **Periodic checkpoints** (`runtime.checkpoint`, issue #244): opt-in `CheckpointConfig` (`every_agent: bool`, `every_seconds: int|None`, `keep_last: int=5`; `is_enabled = every_agent or every_seconds is not None`). Off by default → failure-only behavior preserved. `WorkflowEngine._maybe_save_periodic_checkpoint()` is called once at the **top of `_execute_loop`** (single choke point), where prior outputs are committed and `_current_agent_name` is the step *about to run* — so a periodic checkpoint reuses failure-checkpoint `current_agent` semantics and resume continues forward with no special-casing. Gated via the `_periodic_checkpoints_active` property (**root engine only**, `_subworkflow_depth == 0`, + `is_enabled`) and skips the first iteration (`limits.current_iteration == 0`). The save decision is `_periodic_checkpoint_due(now)` (`every_agent` OR `every_seconds` throttle; first save always fires). `_save_checkpoint_on_failure` and the periodic path share `_write_checkpoint(error, trigger)` (which best-effort-guards provider `get_session_ids()` so it never raises). The periodic save wraps write+emit+rotate; on any failure it calls `_record_periodic_checkpoint_failure()` which emits a **`checkpoint_save_failed`** event (consecutive-failure count; surfaced by `ConsoleEventSubscriber` + JSONL + dashboard) so a recovery-reliant user isn't silently left without checkpoints. After a save the engine calls `rotate_periodic_checkpoints`; at a terminal **non-resumable** outcome (clean completion via `run()`/`resume()`, or an explicit `status: failed` terminate) `_cleanup_run_periodic_checkpoints()` deletes the run's periodic checkpoints (an unexpected failure leaves them in place alongside the failure checkpoint). `conductor checkpoints` shows a `Trigger` column and `—` for periodic rows' error type. See `examples/periodic-checkpoints.yaml` and `docs/workflow-syntax.md` (Periodic Checkpoints section).
- **Skills**: `runtime.skills: [name, ...]` sets a workflow-wide default list of skills enabled for every provider-backed agent; per-agent `skills: [name, ...]` overrides it (tri-state via list presence: omitted = inherit, `skills: []` = explicit opt-out, `skills: [name, ...]` = explicit set). Skill names must resolve to a registered built-in (currently just `conductor`). The observable contract is the same across providers — *"the agent has access to the named skill"* — but the mechanism differs by provider via `AgentProvider.supports_native_skills`: **Copilot** (`True`) registers the skill directory on the SDK session via `skill_directories`, so the agent discovers and loads skill content natively (progressive disclosure via `SKILL.md` frontmatter); **Claude** and **Claude Agent SDK** (`False`) eagerly inject every enabled skill's `SKILL.md` plus `references/*.md` into the agent's rendered prompt inside `<skills><skill name="...">...</skill></skills>` tags. Providers also declare `skills: bool` on their `ProviderCapabilities` descriptor so `conductor validate` can catch skills-against-unsupported-provider mismatches. Built-in skills live under `plugins/conductor/skills/<name>/` and are bundled into the wheel via the hatchling `force-include` entry in `pyproject.toml`. Skills are rejected on non-provider-backed step types (script, wait, set, terminate, workflow, human_gate). See `examples/skills-self-improving-workflow.yaml`.
- **Terminate steps** (`type: terminate`): explicit terminal step with `status` (`success` | `failed`), Jinja2 `reason`, and optional `output_template` (a `dict[str, str]` that replaces `workflow.output:` when set; each value is rendered then passed through `_maybe_parse_json` so `"true"` becomes `True`, `"42"` becomes `42`, JSON literals are parsed). Reaching a terminate step ends the workflow immediately (no routes evaluated after). `success` → CLI exit 0, dashboard ✅, `workflow_completed { termination_reason, terminated_by, is_explicit: true, status }`; runs `on_complete` hook. `failed` → CLI exit 1 (with rendered output JSON still printed to stdout for downstream tooling), dashboard ❌, raises `WorkflowTerminated` (subclass of `ExecutionError`), emits `workflow_failed { error_type: "WorkflowTerminated", is_explicit: true, status, output }`, runs `on_error` hook, and **does not** save an on-failure checkpoint (explicit terminations are intentionally non-resumable). Terminate steps cannot have `routes`, `tools`, `output`, `prompt`, `model`, etc.; cannot be used as parallel-group members or as a for_each inline agent (route to one from those groups' `routes:` instead). Inside a sub-workflow, a `status: failed` terminate is downgraded at the parent boundary to `SubworkflowTerminatedError` (also a subclass of `ExecutionError`) preserving the child's rendered `terminated_output` / `terminated_reason` / `terminated_by` as structured attributes — the parent treats it as a normal sub-workflow failure (its own `workflow_failed` does NOT inherit `is_explicit: true`). For more detail see `examples/terminate.yaml`, `docs/workflow-syntax.md` (Terminate Steps section), and `plugins/conductor/skills/conductor/references/authoring.md`.
- **Structured `runtime.provider` (Copilot custom routing)**: `runtime.provider` accepts either the bare string shorthand (`provider: copilot`) or a structured `ProviderSettings` object that routes the Copilot SDK at OpenAI-compatible / Azure / Anthropic endpoints (Ollama, vLLM, LM Studio, Azure OpenAI, etc.). Object fields: `name` (defaults to `copilot`), `type` (`openai`|`azure`|`anthropic`), `wire_api` (`completions`|`responses`), `base_url`, `api_key`, `bearer_token`, `headers`, `azure.api_version`. `api_key` and `bearer_token` are `SecretStr` (redacted in `model_dump` / dashboard / event logs). The model is frozen after construction. Custom routing activates only when at least one non-`name` field is set in YAML — ambient `OPENAI_*` env vars never divert default routing on their own. Once activated, missing fields fall back from env vars in this order: `base_url` ← `COPILOT_PROVIDER_BASE_URL` → `OPENAI_BASE_URL`; `api_key` ← `COPILOT_PROVIDER_API_KEY` (only — ambient `OPENAI_API_KEY` is intentionally NOT a fallback to avoid credential leaks); `bearer_token` ← `COPILOT_PROVIDER_BEARER_TOKEN`. The schema rejects every non-`name` field when `name != "copilot"` (structured config for other providers is a follow-up). It also rejects anchorless / broken combinations that would silently no-op at the SDK boundary: `wire_api` / `type` / `headers` / `azure` cannot stand alone without `base_url` / `api_key` / `bearer_token`; empty `headers`, empty `SecretStr`, and `azure: {api_version: null}` are rejected. The resolver raises `ProviderError` when custom routing is activated but every resolved field is falsy (e.g. expected env vars all unset). Custom routing applies to both agent execution and dialog turns so all sessions hit the same endpoint. `--provider <name>` CLI override replaces the whole `ProviderSettings` (logs a notice when YAML had structured fields). See `examples/copilot-local-llm.yaml`.
- **Validator block** (`validator:` on a provider-backed agent, issue #220): semantic output validation with retry-once. After the primary agent completes, `WorkflowEngine._apply_validator` runs `OutputValidator` (`engine/validator.py`) — a second LLM call (synthetic agent via `provider.execute`, `tools=[]`, `{passed, issues}` output schema) grading the output against `validator.criteria`. Fields: `criteria` (required, non-empty), `model` (defaults to the agent's model), `max_retries` (`Field(1, ge=0, le=1)` — hard-capped at 1; `0` = report-only). On `passed: false` and `max_retries > 0`, the primary re-runs **once** via `executor.execute` with a `## Validation feedback` section (the issues) appended to `guidance_section`; the second output is final (no second validation loop). **Fail-open**: validator errors / unparseable responses → treated as pass with a logged warning. Wired into the main loop, parallel groups, and for-each loops (guarded by `agent.validator and not output.partial`; for-each passes a `usage_label` so the row matches `<group>[<key>]`). Emits `agent_validator_start` / `agent_validator_complete { passed, issues, errored, tokens, cost_usd }` / `agent_validation_failed { issues, will_retry }` through the per-agent `event_callback` (so for-each events carry `item_key`). Cost: the validation call and any discarded first attempt are recorded as a separate `"<agent> (validator)"` usage row (primary row = effective output). Rejected on `script` / `human_gate` / `workflow` / `wait` / `set` / `terminate` types. Frontend: event types in `web/frontend/src/types/events.ts`, store handlers + `NodeData.validator_*` fields in `web/frontend/src/stores/workflow-store.ts`, detail UI in `web/frontend/src/components/detail/ValidatorDetail.tsx` (rebuild with `make build-frontend`). See `examples/validator.yaml` and `docs/workflow-syntax.md` (Validator section).
Expand Down Expand Up @@ -188,6 +194,7 @@ Tests mirror source structure in `tests/`:
- `test_providers/` - Provider implementation tests
- `test_integration/` - Full workflow execution tests
- `test_gates/` - Human gate tests
- `test_skills/` - Skill registry, loader, schema field, and executor-integration tests

Use `pytest.mark.performance` for performance tests (exclude with `-m "not performance"`).

Expand Down
161 changes: 161 additions & 0 deletions examples/skills-self-improving-workflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Self-improving workflow using the `conductor` skill
#
# This example shows how the generalized `skills:` capability gives an
# agent reusable, version-accurate knowledge of Conductor's YAML
# schema, execution model, and authoring patterns. It's the building
# block that a future `conductor watch` (issue #181) would chain into a
# closed-loop generate → review → fix cycle. The example demonstrates
# all three sides of that loop in one pass.
#
# Provider mechanism — same observable contract, different mechanism:
# * Copilot: the skill directory is registered on the SDK session via
# `skill_directories`. The agent discovers and loads SKILL.md and
# references on demand (progressive disclosure).
# * Claude: SKILL.md + references/*.md are eagerly prepended to the
# agent's rendered prompt inside <skills><skill name="conductor">
# ... </skill></skills> tags.
#
# Tri-state opt-in via list presence:
# * Omit `skills:` → inherit `runtime.skills`
# * `skills: []` → explicit opt-out (e.g. the `formatter` agent
# below, which doesn't need workflow knowledge)
# * `skills: [name]` → explicit set, replaces the workflow default
#
# Usage:
# conductor run examples/skills-self-improving-workflow.yaml \
# --input task="Write a workflow that summarises a GitHub issue."

workflow:
name: skills-self-improving-workflow
description: |
Generate a Conductor workflow, review it for correctness against
the bundled `conductor` skill, then revise it based on the review.
Demonstrates the building-block pattern for future iterative
fix-validate loops (see issue #181).
version: "1.0.0"
entry_point: author

runtime:
provider: copilot
skills: [conductor] # every provider-backed agent inherits this

input:
task:
type: string
required: true
description: A short description of the workflow the author should generate.

agents:
- name: author
description: Drafts an initial Conductor workflow for the given task.
prompt: |
You are authoring a brand-new Conductor workflow.

Task: {{ workflow.input.task }}

Produce a complete, runnable workflow YAML that solves the task.
Use the `conductor` skill for schema details, allowed step types,
routing patterns, and naming conventions. Prefer a minimal,
idiomatic design over an exhaustive one.
output:
workflow_yaml:
type: string
description: The drafted workflow YAML.
notes:
type: string
description: Author's notes on design choices.
routes:
- to: reviewer

- name: reviewer
description: Reviews the drafted workflow for schema and design issues.
prompt: |
Review the following Conductor workflow YAML.

```yaml
{{ author.output.workflow_yaml }}
```

Use the `conductor` skill to verify:
* Schema correctness (field names, types, required fields)
* Routing soundness (no orphan agents, no unreachable routes,
terminating routes present)
* Sensible context mode and failure-mode choices
* Appropriate use of step types (agent / script / set / wait /
terminate / parallel / for_each / human_gate / workflow)

List blocking issues separately from suggestions. If the workflow
is already correct and idiomatic, say so plainly and return an
empty `issues` list.
output:
issues:
type: string
description: Blocking schema or correctness issues. Empty if none.
suggestions:
type: string
description: Non-blocking improvements.
verdict:
type: string
description: One of "approved" or "needs_revision".
routes:
- to: fixer
when: "{{ reviewer.output.verdict == 'needs_revision' }}"
- to: formatter

- name: fixer
description: Applies the reviewer's findings and produces a revised workflow.
prompt: |
The reviewer flagged issues with the drafted workflow.

Original:
```yaml
{{ author.output.workflow_yaml }}
```

Review findings:
- Issues: {{ reviewer.output.issues }}
- Suggestions: {{ reviewer.output.suggestions }}

Use the `conductor` skill to produce a corrected, complete
workflow YAML that addresses every blocking issue. Adopt
suggestions where they materially improve the design. Output the
final YAML and a brief changelog.
output:
workflow_yaml:
type: string
description: The revised workflow YAML.
changelog:
type: string
description: Short summary of what changed and why.
routes:
- to: formatter

- name: formatter
description: Renders the final workflow + report. Doesn't need the skill.
# Explicit opt-out: this agent only formats existing strings. No
# need to bloat its prompt with the full skill content.
skills: []
prompt: |
Produce a Markdown report containing two sections:

## Workflow

```yaml
{{ fixer.output.workflow_yaml | default(author.output.workflow_yaml) }}
```

## Notes

Original notes: {{ author.output.notes }}
Review verdict: {{ reviewer.output.verdict }}
{% if fixer.output.changelog is defined %}
Changelog: {{ fixer.output.changelog }}
{% endif %}
output:
report:
type: string
description: The final Markdown report.

output:
report: "{{ formatter.output.report }}"
verdict: "{{ reviewer.output.verdict }}"
Loading
Loading