Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
30 changes: 30 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,19 @@ All providers must maintain feature parity where applicable. Any change to one p

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.

#### `openai.py` parity notes

The OpenAI provider (`openai.py`) implements `AgentProvider` on the shared
Pydantic AI execution loop (`src/conductor/providers/_pydantic_ai/runner.py::run_agent_pipeline`).
It shares toolset bridges, event callbacks, interrupts, and retry contracts with `ClaudeProvider`,
with the following backend specifics:

- **Shared runner**: Delegates `execute()` to `run_agent_pipeline()` with an `OpenAIChatModel` backend.
- **Env-resolution rule**: YAML `api_key` and `base_url` take precedence over `OPENAI_API_KEY` and `OPENAI_BASE_URL`. Ambient env vars never reroute an unconfigured provider (e.g. `provider: copilot` is never diverted by `OPENAI_*` variables). Once a custom `base_url` is in effect — from YAML **or** from `OPENAI_BASE_URL` — the ambient `OPENAI_API_KEY` is **not** forwarded to it and construction raises `ValidationError` unless `api_key` was passed explicitly, so a personal credential never reaches an endpoint the author did not pair it with (the same reasoning behind `copilot.py` refusing ambient `OPENAI_API_KEY`). `self._api_key` is deliberately left `None` on the ambient path rather than being written back, which keeps `agent_builder.py`'s own guard reachable in production instead of only under its unit test.
- **Chat-Completions-only**: Exclusively uses the OpenAI Chat Completions API; the OpenAI Responses API is not supported. This is also why `CAPABILITIES.agent_reasoning_events` is `False`: pydantic-ai only builds a `ThinkingPart` from `reasoning`/`reasoning_content`, a DeepSeek/Moonshot field, so `api.openai.com` never emits `agent_reasoning` on this backend. It would become `True` on an `OpenAIResponsesModel` backend.
- **Reasoning effort**: Declares `("low", "medium", "high")` and rejects `xhigh`/`max` with a `ValidationError`. `xhigh` arrived with a later model generation than the o-series and is unverified against arbitrary OpenAI-compatible endpoints, so the provider declares the narrower tuple (matching the way `hermes.py` omits `max`). Effort is additionally validated *per model* at agent-build time via `pydantic_ai.profiles.openai.openai_model_profile(...).openai_supports_reasoning`, so a non-reasoning model like `gpt-4o` fails before the request instead of returning a 400 mid-run; an unavailable profile attribute yields `None`, which skips the check rather than guessing.
- **Temperature 0..2**: The per-provider ceiling lives on `ProviderCapabilities.max_temperature` (`1.0` for copilot/claude/hermes/aca/claude-agent-sdk, `None` for openai) and is enforced by `factory.py::_enforce_temperature`, the choke point `run`, `resume` and `validate` all share — the schema bound alone would not cover `conductor run`, which never calls the cross-reference validator.

#### `claude_agent_sdk.py` parity notes

The Claude Agent SDK provider (`claude_agent_sdk.py`) is the canonical
Expand Down Expand Up @@ -637,3 +650,20 @@ Implementation parity rules:
- 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 is seeded with prior events before it starts accepting clients. The CLI prepends a fresh `workflow_started` event built from the **current** workflow YAML (via `WorkflowEngine.build_workflow_started_data()`) so historical events apply to the correct topology; it then either replays the original JSONL event log (`WebDashboard.replay_events_from_jsonl()` — when the checkpoint records an `event_log_path` and the file exists) or synthesises minimal `*_started` / `*_completed` pairs from the restored `WorkflowContext` (`replay_synthetic_from_context()`). The resumed engine's own `workflow_started` emit is suppressed via `engine.suppress_workflow_started_emit()` so the dashboard sees exactly one root `workflow_started` (no `wfDepth` double-count). Two disjoint sets of events are filtered on replay. `_REPLAY_ROOT_SKIP_TYPES` (`workflow_started` / `workflow_completed` / `workflow_failed` / `checkpoint_saved` / `checkpoint_save_failed`) is filtered **only at root depth** — subworkflow-level lifecycle events are preserved so frontend `wfDepth` stays balanced. `_REPLAY_INTERACTIVE_SKIP_TYPES` (`agent_paused` / `agent_resumed` / `iteration_limit_reached` / `iteration_limit_resolved` / `dialog_started` / `dialog_completed`) is filtered at **every depth**, because the control channel is the root dashboard's `resume_event` / `kill_event` / gate id no matter which engine emitted the event. Each of those sets a *global* store latch (`isPaused`, `iterationLimitGate`, `activeDialog`) that only its counterpart event can clear — plus, for `isPaused`/`iterationLimitGate` only, a root terminal event, which the first set filters — so replaying the opening half of an unresolved pair latches it on for the whole resumed run. Concretely, a run stopped then killed from the dashboard logs `agent_paused` with no `agent_resumed`, so replaying it renders Resume/Kill instead of Stop on the live resumed run, hiding the only graceful stop behind a Kill that hard-stops a healthy workflow. A pause or gate the resumed run genuinely re-enters emits its own fresh event; `dialog_message` is left unfiltered only because it is *inert* once `dialog_started` is filtered — both renderers of `node.dialog_messages` (`DialogEngagementPrompt` via `DetailPanel`, `DialogDetail` via `DialogOverlay`) gate on `dialog_active`/`activeDialog`, which only `dialog_started` sets, so replayed dialog transcripts are **not** visible on a resumed dashboard. `components/layout/Header.tsx` additionally hides all live-control buttons when `replayMode` is set, since `ReplayDashboard` serves no `/api/stop`|`/api/resume`|`/api/kill`; `replayMode` is latched in `hooks/use-replay.ts` on mount (that hook only mounts once `App`'s `/api/replay/info` probe confirmed replay) rather than when `/api/state` resolves, so a slow or failed event fetch cannot leave the live controls rendered. The resumed `EventLogSubscriber` opens the original JSONL in append mode (when available) so a multi-resume session produces one continuous log file and `run_id` stays stable for log-correlation tools.

## Pydantic AI Runner Extraction

The shared Pydantic AI execution loop from `ClaudeProvider.execute()` was extracted into `src/conductor/providers/_pydantic_ai/runner.py::run_agent_pipeline()`. `ClaudeProvider.execute()` is now a thin wrapper that:

1. Resolves the MCP manager for the agent's working directory.
2. Builds a backend-specific `build_agent_fn(toolsets, *, max_parse_recovery_attempts)` closure.
3. Converts the provider-level `RetryConfig` to the internal `PydanticRetryConfig` representation.
4. Delegates to `run_agent_pipeline(...)`.

The runner owns the toolset wiring, retry/interrupt/extract pipeline, partial-output construction, and model-name resolution. To keep existing tests green, the runner imports its Pydantic-AI seam helpers (`run_with_interrupt`, `execute_with_retry`, `extract_content`, etc.) **inside** `run_agent_pipeline()` rather than at module scope. This preserves the historical patching surface (`conductor.providers._pydantic_ai.interrupt.run_with_interrupt`, etc.) that tests rely on.

## Provider registration and validation notes

- **Provider-aware temperature bounds**: `RuntimeConfig.temperature` widens the schema upper bound to `2.0` because OpenAI supports it, but most providers (copilot, claude, hermes, aca, claude-agent-sdk) cap at `1.0`. `config/validator.py` enforces the per-provider bound at `conductor validate` time so high temperatures do not fail mysteriously at the SDK boundary. Only the `openai` provider is exempt; override agents to `openai` or keep `temperature <= 1.0` for the others.
- **Provider registration completeness**: adding a new provider requires updating `providers/factory.py::ProviderType` and `providers/diagnostics.py::_CREDENTIAL_SPECS` (and `providers/capabilities.py::_PROVIDER_CLASS_PATHS` for validate-time capability checks). Latent forwarding bugs happen when `ProviderRegistry.get_or_create_provider` does not pass new runtime fields to `create_provider`.
- **OpenAI provider routing restrictions**: `ProviderSettings` for `name="openai"` rejects `type`, `wire_api`, `bearer_token`, `headers`, `azure`, and Copilot runtime fields with targeted messages because the native OpenAI provider always speaks Chat Completions and does not support Copilot custom routing.
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/microsoft/conductor/compare/v0.1.33...HEAD)

### Added

- **Native OpenAI provider** — new stable `openai` provider built on the shared
Pydantic AI runtime. Works with the real OpenAI API and any OpenAI-compatible
Chat Completions endpoint (Ollama, vLLM, LM Studio, OpenRouter, corporate
proxies). Supports MCP tools, structured output, interrupts, reasoning effort
(`low`/`medium`/`high`), and the full `0.0`–`2.0` temperature range, which is
now expressed as `ProviderCapabilities.max_temperature` and enforced in
`create_provider` so `conductor run` and `conductor resume` are covered rather
than only `conductor validate`. See `docs/providers/openai.md` and
`examples/openai-compatible.yaml`.

A custom `base_url` requires an explicit `api_key`: an ambient `OPENAI_API_KEY`
is never forwarded to a non-OpenAI endpoint.

### Changed

- The Pydantic AI dependency was narrowed from the full `pydantic-ai` package to
`pydantic-ai-slim[anthropic,openai]`. This drops the bundled `pydantic_ai.mcp`
module, which Conductor replaces with its own toolset bridge, so the change is
transparent to users.
- The previously reserved `openai-agents` provider name has been removed from the
schema, factory, registry and diagnostics. Workflows that named it now fail at
schema load time rather than at the first agent execution.

### Fixed

- Retry classification now covers the `ModelHTTPError` and `ModelAPIError` types
pydantic-ai actually raises, so `408`, `429` and `5xx` responses are retried on
the Claude provider as well as the new OpenAI one. Previously they were treated
as fatal.
- `runtime.default_reasoning_effort` was silently dropped at run time for every
provider and is now forwarded through `ProviderRegistry`.
- **MCP tool discovery and structured tool results no longer break with MCP
2.0** (#419). MCP 2.0 renamed the Python field on `mcp.types.Tool` from
`inputSchema` to `input_schema` and on `mcp.types.CallToolResult` from
Expand Down
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,14 +346,14 @@ See [docs/fleet.md](docs/fleet.md) for every screen, key binding, the status voc

Conductor supports multiple AI providers. Choose based on your needs:

| Feature | Copilot | Claude | Claude Agent SDK | Hermes | ACA |
|---------|---------|--------|------------------|--------|-----|
| **Tier** | Stable | Stable | Experimental | Experimental | Experimental |
| **Pricing** | Subscription | Pay-per-token | Subscription | Pay-per-token (via hermes) | Subscription + ACA compute |
| **Context Window** | Per-model | Per-model | Per-model | Per-model | Per-model (inner Copilot) |
| **Tool Support (MCP)** | Yes | Yes (stdio) | Yes (built-in) | No (hermes internal tools) | Yes (always forwarded, not allowlisted) |
| **Streaming** | Yes | Yes | Yes | No | Yes |
| **Best For** | Heavy usage, tools | Large context, pay-per-use | Full Claude Code toolset | Multi-provider model access | Untrusted/isolation-sensitive agents |
| Feature | Copilot | OpenAI | Claude | Claude Agent SDK | Hermes | ACA |
|---------|---------|--------|--------|------------------|--------|-----|
| **Tier** | Stable | Stable | Stable | Experimental | Experimental | Experimental |
| **Pricing** | Subscription | Pay-per-token | Pay-per-token | Subscription | Pay-per-token (via hermes) | Subscription + ACA compute |
| **Context Window** | Per-model | Per-model | Per-model | Per-model | Per-model | Per-model (inner Copilot) |
| **Tool Support (MCP)** | Yes | Yes (stdio) | Yes (stdio) | Yes (built-in) | No (hermes internal tools) | Yes (always forwarded, not allowlisted) |
| **Streaming** | Yes | Yes | Yes | Yes | No | Yes |
| **Best For** | Heavy usage, tools | OpenAI ecosystem, pay-per-use | Large context, pay-per-use | Full Claude Code toolset | Multi-provider model access | Untrusted/isolation-sensitive agents |

### Using Copilot

Expand All @@ -366,6 +366,17 @@ workflow:

Copilot is the default provider — `runtime.provider` can be omitted entirely. Requires an active GitHub Copilot subscription and the GitHub CLI authenticated (`gh auth login`).

### Using OpenAI

```yaml
workflow:
runtime:
provider: openai
default_model: gpt-5-mini
```

Set your API key: `export OPENAI_API_KEY=sk-...`

### Using Claude

```yaml
Expand Down Expand Up @@ -415,7 +426,7 @@ workflow:

The `aca` provider delegates an agent's entire agentic loop, tools, and MCP calls to a remote **Azure Container Apps dynamic-sessions sandbox** instead of running it on the host — useful for untrusted or isolation-sensitive agents (e.g. running arbitrary generated code). Unlike the other providers, `aca` requires the structured `provider:` form with a `pool_endpoint` pointing at an operator-provisioned ACA session pool (`scripts/aca/provision-pool.sh`) and `azure-identity` for authentication. Resolves its inner Copilot credential automatically via `COPILOT_PROVIDER_BASE_URL` → `COPILOT_GITHUB_TOKEN`/`GH_TOKEN`/`GITHUB_TOKEN` → `gh auth token`, so a `gh`-authenticated operator needs no ACA-specific setup. See [`examples/aca-coding-agent.yaml`](examples/aca-coding-agent.yaml) for a full end-to-end example.

**See also:** [Claude Documentation](docs/providers/claude.md) | [Hermes Documentation](docs/providers/hermes.md) | [ACA Documentation](docs/providers/aca.md) | [Provider Comparison](docs/providers/comparison.md) | [Migration Guide](docs/providers/migration.md)
**See also:** [OpenAI Documentation](docs/providers/openai.md) | [Claude Documentation](docs/providers/claude.md) | [Hermes Documentation](docs/providers/hermes.md) | [ACA Documentation](docs/providers/aca.md) | [Provider Comparison](docs/providers/comparison.md) | [Migration Guide](docs/providers/migration.md)

### Using a Local / Custom LLM Endpoint (Ollama, vLLM, Azure OpenAI, ...)

Expand Down Expand Up @@ -550,6 +561,7 @@ See the [`examples/`](./examples/) directory for complete workflows:
| [Fleet Manager](./docs/fleet.md) | `conductor fleet` TUI: screens, key bindings, gate resolvability, retention |
| [Parallel Execution](./docs/parallel-execution.md) | Static parallel groups |
| [Dynamic Parallel](./docs/dynamic-parallel.md) | For-each groups and array processing |
| [OpenAI Provider](./docs/providers/openai.md) | OpenAI setup and configuration |
| [Claude Provider](./docs/providers/claude.md) | Claude setup and configuration |
| [Hermes Provider](./docs/providers/hermes.md) | Hermes setup and configuration |
| [ACA Provider](./docs/providers/aca.md) | Azure Container Apps sandboxed execution setup and configuration |
Expand Down
19 changes: 9 additions & 10 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -903,11 +903,10 @@ in the `env` section (cache-first, short timeout, silent, and skipped when
- **env** — Conductor version, Python version, OS/platform, and update
availability.
- **providers** — for each known provider (`copilot`, `claude`,
`claude-agent-sdk`, `hermes`, `openai-agents`): whether the SDK is
installed, the capability tier (`stable` / `experimental`), which
credential environment variables are **present** (presence only — values
are never printed), and — with `--check` / `--models` — connection status
and a model count. `openai-agents` is surfaced as "not yet implemented".
`claude-agent-sdk`, `hermes`, `openai`): whether the SDK is installed,
the capability tier (`stable` / `experimental`), which credential environment
variables are **present** (presence only — values are never printed), and
— with `--check` / `--models` — connection status and a model count.
- **registries** — configured workflow registries and which is the default
(see [`conductor registry`](#conductor-registry)).

Expand Down Expand Up @@ -940,10 +939,10 @@ Coverage varies by provider — every field degrades independently to `n/a` /
none) and reports only `Prompt` (via the Anthropic API's
`max_input_tokens`) — `Output` and `Context` are always `—` and `Default`
is always `—` (Anthropic has no per-model default-effort concept).
- **`claude-agent-sdk`**, **`hermes`**, and **`openai-agents`** don't
implement model enumeration (`list_models`) at all, so `--models` shows
`n/a` for them in the Providers table and they get **no** Models detail
table — there is nothing to detail.
- **`claude-agent-sdk`** and **`hermes`** don't implement model
enumeration (`list_models`) at all, so `--models` shows `n/a` for them in
the Providers table and they get **no** Models detail table — there is
nothing to detail.

In `--json`, each provider's `models` field is a list of objects (not plain
id strings):
Expand Down Expand Up @@ -973,7 +972,7 @@ values are never read or printed. Detected variables per provider:
| `claude` | `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` | required (direct Anthropic API) |
| `claude-agent-sdk` | `ANTHROPIC_API_KEY` | optional override — authenticates via `claude login` |
| `hermes` | *(none — endpoint / API key are passed explicitly)* | — |
| `openai-agents` | *(none — not yet implemented)* | — |
| `openai` | `OPENAI_API_KEY` | required (direct OpenAI API) |

For **`copilot`** and **`claude-agent-sdk`**, these env vars are *optional
overrides*: both providers authenticate primarily via an on-disk CLI login
Expand Down
Loading
Loading