All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Direct MCP workflow steps (
type: mcp) (#392): calls a tool on a configuredruntime.mcp_serversstdio server directly without an LLM. Arguments are rendered recursively with Jinja2 and auto-coerced to JSON-native types; the result envelope (content,structured,is_error) merges structured keys directly onto the output dict so routes and downstream steps can branch onoutput.is_erroror individual fields. Calls serialize per server process to maintain stdio stream integrity while distinct servers execute concurrently in parallel groups. Output text payload is bounded byruntime.tool_outputwith spill-to-file support while structured data is preserved intact. Step and result values are excluded from all lifecycle events (mcp_started,mcp_completed,mcp_failed), with failure messages redacted to a safe category and full exception traces written only to a private per-run*.mcp-diagnostics.logfile (named by the redacted message). Seedocs/workflow-syntax.mdandexamples/mcp-step.yaml. - OpenTelemetry spans for direct MCP workflow steps: each
type: mcpexecution is exported as anexecute_toolspan under its workflow, parallel group, or for-each item. Spans include bounded server, tool, result-size, and truncation metadata without recording arguments, result contents, or spill paths, and preserve routed tool errors, execution failures, and interrupted attempts as distinct outcomes.
- Pydantic AI structured-output agents explicitly require
final_result— the generated output tool now tells models that they must call it before finishing and that plain-text responses are not accepted. This improves adherence for local models behind OpenAI- or Anthropic-compatible endpoints without replacing tool-based output, weakening schema validation, or changing authored system prompts.
0.1.37 - 2026-09-09
-
Always-on client-side context compaction for the
claudeandopenaiproviders (#503) — an agent whose conversation outgrows the model's context window no longer fails the run. Conductor condenses the history once it crosses a calculated trigger threshold and continues. The trigger is computed from an additive reserve formula — context window minus output limit minus an effective tool-output-derived buffer (clamped to at most 25% of the window) — and compaction is disabled, with adisabled_reasonon theagent_compaction_configevent, when the remaining trigger would fall below 4096 tokens. Condensing runs a three-stage strategy: clearing old tool results first, summarizing older messages with a nested model call, and sliding the window as a deterministic fallback. It is client-side only, so behaviour is identical behind an API proxy, always on with no new YAML surface, and fail-open — a compaction failure never aborts a run, and an unrecovered one disables compaction for the rest of that agent execution. Every compaction is surfaced in the console, the JSONL event log, and the web dashboard. -
Provider-advertised model token-limit metadata (#503) for compaction sizing. The
claudeandopenaiproviders read per-model input/output token limits from their SDK model listings (with full pagination for the Anthropic SDK, and a vendor-field parser for OpenAI-compatible endpoints), thecopilotprovider implements theget_max_output_tokenshook, and resolution falls back through thegenai-pricesregistry to a conservative default. -
Opt-in
runtime.provider.setting_sourcesonclaude-agent-sdk(#501) — selects which Claude Code settings tiers (user/project/local) a session may load. It is empty by default, so behaviour is unchanged unless a workflow asks: the provider still sends an explicit[], which is load-bearing because the SDK re-defaults an unset value to["user", "project"]wheneverskillsis set. The case it exists for is an agent whoseworking_diris a target repository shipping its own.claude/skills— the SDK has--plugin-dirbut no--skill-dir, so that repo otherwise has to package its skills as a Claude Code plugin;[project]reads them, and the repo'sCLAUDE.md/.claude/ruleswith them. An enabled tier brings that tier's hooks, so it is only for repositories trusted as much as the workflow itself; the field is rejected on every other provider name rather than accepted and silently ignored, counts as structured config (so--provideroverrides warn before discarding it and-vshows it), and a per-agentskills: []opts that agent out of the tiers entirely. Seeexamples/claude-agent-sdk-setting-sources.yaml. -
OpenTelemetry tracing, new opt-in OpenTelemetry tracing feature activated by
OTEL_EXPORTER_OTLP_ENDPOINT. It instruments workflow orchestration to emit spans for workflows, agents, parallel/for-each groups, steps, and tool executions. Forclaudeandopenaiproviders, it also registers and enables native Pydantic AI instrumentation; forcopilot, it captures native spans from the Copilot CLI child process over OTLP HTTP. In each case Conductor unifies orchestrator spans and LLM/tool calls into a single trace tree. Tracing is enabled by configuring standardOTEL_*environment variables for an OTLP collector. Seedocs/telemetry.mdandexamples/telemetry.yaml.- Added native Copilot CLI spans over OTLP HTTP using W3C trace-context propagation, backed by a per-run protocol and endpoint latch that logs a warning if gRPC is used.
-
Per-agent
settings_dironclaude-agent-sdk(#513) — selects which directory'sprojectsettings tier supplies an agent's skills, independently ofworking_dir. The CLI advertises exactly one MCP root — its cwd — and a filesystem MCP server that sees a Roots-capable client discards the directories in its own argv, so pointingworking_dirat a target repository to pick up its skills also narrowed the agent's only MCP root onto it.settings_dirsplits the two, letting cwd stay wide enough for every path the agent must read. It carries a second, unconditional effect:add_dirswidens the model's built-inRead/Edit/Bashto that tree regardless of any settings tier, though no Conductor configuration reaches a permission mode where that is observable today. Only the skills of that directory travel — notCLAUDE.md,.claude/rules/*.md,.claude/settings.jsonor.claude/agents, all measured. Refused atconductor validateand at run time on a provider that cannot apply it; asettings_dirwhoseprojecttier is not enabled warns in both places too, since the filesystem grant applies even when the skills half no-ops. Reported on the agent lifecycle events so the grant is auditable. Seeexamples/claude-agent-sdk-settings-dir.yaml.
-
Claude default
max_tokensraised from 8192 to 16384 when unset (#503). This doubles the worst-case output cost per call for users who never set it; setruntime.max_tokensexplicitly to keep the former behavior. For Claude thinking agents,lowormediumeffort levels without an explicitmax_tokenslimit now send 16384 tokens instead of the former 8192 or 12288 tokens. -
The
openaiprovider honors vendor-advertised token limits (#503) from the models listing when available, using them to size the compaction output reserve. -
A
working_dirorsettings_dirtemplate that renders empty is now an error (#513). Previously an empty render resolved to the workflow file's own directory —Path("")isPath("."), which is not absolute, so it was joined onto that directory and passed the existence check — and the agent ran there. A value meaning "nothing" silently becoming something real is the defect; forsettings_dirit would also have granted the model access to the workflow's own tree. Both fields now fail before the provider call, naming the field and the template it came from.
-
openai: retry transient errors delivered inside an SSE stream (#506) — the OpenAI SDK raises a bareopenai.APIError(no HTTP status) for anerrorobject embedded in a stream, which pydantic-ai does not translate, so a configuredretry:policy was skipped and the run failed after the first attempt. Now retried: OpenAI mid-stream 5xx (server_error/internal_server_error), OpenAI rate limits (typerequests/tokenswith coderate_limit_exceeded), Anthropic-shaped gateway errors proxied unchanged (rate_limit_error/overloaded_error/api_error), and stream errors with no parseable payloadtype(a non-objecterrorvalue from an Ollama/vLLM gateway, or an Azure-style{"code": ...}shape), which are treated like broken streams. Still fatal: recognized client-side payload types (e.g.invalid_request_error) and every HTTP 4xx. Errors a narrowedretry_on:declines are now wrapped inProviderErrornaming the declined category instead of escaping as raw SDK exceptions, a declined retry is logged at warning level (a taken one already was), and a fatal bareAPIError's message now carries the payloadtype/codethe SDK leaves out ofstr(e). -
Context compaction window guard against token-dense drift (#507) — the
claude/openaiproviders' compaction trigger anchors on provider-reported token usage and estimates everything after the anchor with a ~4-characters-per-token heuristic, which undercounts token-dense content (CJK and other non-Latin scripts, base64, hex, minified data) by 2-4x. A dense suffix could therefore grow the real request past a known context window while the trigger estimate stayed below the threshold, and the provider rejected the request withcontext_length_exceeded. A second, density-calibrated estimate now guards the hard window: it matches the primary heuristic on ordinary prose, counts text with a substantial non-ASCII share at ~1 token per character, and whitespace-poor ASCII blobs at ~2 characters per token, so it fires only on genuinely dense content — never on a history that is merely large. When it fires, the tier chain is driven directly against that measurement (the inner strategy's own gate would re-measure with the same heuristic that under-counted the content and no-op), until the estimate is back under the target. Telemetry stays on the token scale:agent_compaction_startgainstrigger_reason("trigger"/"window_guard") and a separatedensity_tokensfield instead of overloadingtokens_before, andagent_compaction_completegainsdegraded_estimatorsandstill_over_windowso a guard compaction that could not get back under the window reads as degraded, not as false success. A failed primary measurement falls back to an independent density-calibrated estimate that shares no code with it, and a double failure is reported as a newagent_compaction_skippedevent (reason: "estimate_unavailable") rather than vanishing into stderr. See Workflow Syntax → Context Compaction. -
Validator retries preserve the primary agent conversation (#511) — when a semantic validator rejects output from the Claude, OpenAI, or Hermes provider, the correction now continues the completed agent conversation — the Pydantic AI message history for Claude/OpenAI, the run's own message list for Hermes — and sends validation feedback as the next user turn. This preserves prior reasoning and tool exchanges without repeating the original prompt, workspace instructions, or injected skills. A failed re-run now reports its cause on the
agent_validation_failedevent and in the console log, and the dashboard keeps the agent's original prompt visible instead of replacing it with the feedback-only turn. -
Concurrent agents no longer build duplicate provider instances (#512) — resolving one provider type from two agents at once (a parallel group, or a
for_eachwithmax_concurrent > 1) was a check-then-act with anawaitbetween the cache check and the cache write, so each could construct its own instance and the second write replaced the first, leaving the two agents holding different objects for the same provider type. A waiter could also observe a provider before its restored resume-session state had been applied. Construction, resume-session wiring, and cache publication are now one operation under a registry-local lock that re-checks the cache; cached reads stay lock-free, a failed construction caches nothing and strands no waiter, and distinct provider types remain independent. -
A multi-line reply to a terminal dialog is now one turn (#509) — dialog mode was the only free-text human-input surface that could not accept a multi-line answer (
QuestionDef.multilinedefaults toTrueandGateOption.multilineopts in, both served by one reader ingates/human.py). The dialog gate read a reply with single-linePrompt.ask, so pasting a block of text into an interactive terminal dispatched each line as its own turn: a three-line paste became three separate questions to the model, each answered against a fragment, and the paste's trailing newline added a fourth turn with empty content. Terminal turns now read through the multi-line reader already behind the human gate's.sentinel, submitted with/sendon its own line, so internal newlines survive and a paste is a single message. An empty or whitespace-only submission is no longer dispatched as a turn. A dismiss keyword is recognised only once a turn is submitted, so on a ttydonenow needs/sendafter it, and both the opening banner and the failure-recovery notice say so rather than naming a keystroke that does nothing there.Ctrl-D at the start of a line (Ctrl-Z then Enter on Windows) also submits the lines entered so far, or dismisses the dialog when there are none. Because a terminal's EOF does not persist, abandoning a part-written reply that way now sends what was already entered and a second Ctrl-D is needed to leave, where one used to exit; on an empty prompt it still exits in one keystroke.
The dialog uses
/sendwhere the human gate keeps., since a lone.is likelier to be prose in a conversational reply. Off a tty — a pipe or CI — replies are still read one line at a time and/sendhas no effect; the one change on that path is that a blank line is now skipped instead of dispatched as an empty turn. The web dashboard is unaffected: it takes a separate path that already delivered each message whole.
0.1.36 - 2026-09-02
conductor mcp serve(#432) — exposes your registered Conductor workflows as MCP tools to any MCP-compatible host (Claude Code, VS Code, Cursor, etc.) over stdio, with no workflow edits required: every workflow in every configured registry is exposed by default, with a typedinputSchemaderived from its owninput:block. A tool call always forks a real detachedconductor run— the server never executes a workflow in-process — and by default returns immediately with a run handle carrying therun_id, dashboardurlandport, the captured log paths, and theconductor fleet/conductor statuscommands for watching it from a terminal, so the caller can report the run back and move on while it keeps going. A caller may opt into a bounded wait per call (_wait_seconds, capped by--max-wait-seconds), which changes only whether that call blocks — never how the workflow runs; start the server with--max-wait-seconds 0to make every invocation non-blocking regardless of what a caller requests. A run that has not completed (immediate, at-gate, failed, or timed-out) returns the handle; a run that completes within a bounded wait returns its output inline, or — once serializedoutput:exceeds 50 KB — spilled to a file with aresource_linkand no dashboardurl. Newconductor_run_status/conductor_await_run/conductor_cancel_run/conductor_list_runstools answer for arun_idbefore, during, at a human gate, and after a run has finished — the human gate is never auto-skipped; a run that reaches one parks and reports its dashboard approval URL until a person resolves it. Optionalintrospect/diagnosetoolsets (off by default; enable with--toolsets) add event-query, per-step detail,conductor doctor/conductor validateequivalents, and links (never file contents) to a run's raw logs.--allow/--denynarrow or force the exposed set; a registry above--max-direct-tools(default 25) degrades to a two-tool discovery pair instead of failing or overflowing a host's tool-count limit. Seedocs/mcp-server.mdfor the full guide, including a dedicated Limits section for what this release deliberately does not do (nooutputSchema, no Streamable HTTP transport, tool call payloads withheld unless--introspect-full).workflow.mcp:block — per-workflow configuration read byconductor mcp serveto decide how a workflow is exposed as an MCP tool:expose(defaulttrue),mode(async/sync/auto),read_only,destructive, andestimated_minutes. Every field defaults to the value that keeps an existing workflow with nomcp:block at all exposed identically to one that declares the defaults explicitly, so no existing workflow needs editing. An unknown key inside the block is aconductor validateschema error, not a silently ignored typo. Seeexamples/mcp-serve.yaml.
conductor statusandconductor fleet listnow also list recently-completed runs, not just currently-running ones — a contract change to what these commands mean. Previously both meant "runs alive right now"; a finished run disappeared the moment its process exited. Each command now renders (or, forstatus --json, returns in an additivecompletedarray) a bounded set of recently-completed runs with their terminal status (completed/failed), when they ended, duration, tokens/cost, and error type for a failure — sourced from the terminal run record every run now writes on exit.conductor fleet list's completed rows are bounded by[fleet.retention].keep_last. Both commands remain read-only: listing a completed run never removes its terminal record. Pass--liveto either command to restore the exact previous scope (status --json --livealso drops thecompletedkey from the payload entirely, so an existing scripted consumer ofpayload["running"]is otherwise unaffected by this change). The Fleet Manager TUI's History screen also gained a failed run's error message and a completed run's rendered output, reachable by selecting a row, without adding a new column to its table.
- The
mcpSDK dependency is now bounded below its breaking 2.0 release (mcp>=1.28.1,<2).mcp2.0.0 renamed the camelCase attributes the existing MCP client reads (Tool.inputSchema->input_schema), so an installation whose lock had already floated tomcp2.x had a client that connected to a server and then raisedAttributeErroron every tool listing — MCP tools were silently non-functional. Re-locking with this bound restores them; the pin does not change behavior for anyone already onmcp1.x.
0.1.35 - 2026-08-28
- Dead workflow lifecycle hooks (
on_start/on_complete/on_error) (#476). Thehooks:block was parsed and its templates rendered, but the result was discarded — never emitted as an event, logged, shown, stored, or used for any side effect — so the feature was entirely unobservable, including when a hook failed.HooksConfig,WorkflowDef.hooks, the engine's_execute_hook/LifecycleHookResultand all call sites, and the Hooks section ofdocs/workflow-syntax.mdhave been removed. A workflow that still declareshooks:now fails validation with a clear error rather than silently ignoring the block. Lifecycle hooks may return later, but the right syntax will be designed against a concrete requirement (emitting an event, invoking atype: scriptstep, or calling a webhook) rather than rendering a template and throwing it away.
conductor doctor's table output no longer dies part-written on acp1252console (#401). The Installed/Credentials/Connection/Models columns hardcoded✓/✗/○/⚠, none of which cp1252 can encode, so a run on a legacy Windows console raisedUnicodeEncodeErrormid-table, after the Environment section had already printed.conductor doctornow resolves each glyph once per invocation against the output console's stream encoding, falling back toOK/X/o/!when the Unicode glyphs cannot be encoded; the--jsonpath was already safe and is unchanged.- Plugin flavor resolution (Claude vs. Copilot builds) (#497). A
Claude-built plugin's
agents/*.mdsubagents (no.agent.mdsuffix) were silently never loaded — the candidate-file rule was hardcoded to the Copilot build's convention. Flavor is now read off the manifest that actually matched and threaded as a tie-break-only axis through plugin resolution, soprovider: copilotagents using a Claude-built plugin now get its subagents too. Also adds~/.copilot/settings.jsonmarketplace resolution as a fallback forplugin@marketplacereferences, and several new non-fatal warnings when a build cannot be determined unambiguously.
0.1.34 - 2026-08-24
-
Native OpenAI provider — new stable
openaiprovider 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 full0.0–2.0temperature range, which is now expressed asProviderCapabilities.max_temperatureand enforced increate_providersoconductor runandconductor resumeare covered rather than onlyconductor validate. Seedocs/providers/openai.mdandexamples/openai-compatible.yaml.A custom
base_urlrequires an explicitapi_key: an ambientOPENAI_API_KEYis never forwarded to a non-OpenAI endpoint. -
Fleet TUI launch directory (#477) —
conductor fleetcan now start runs in a directory other than the one it was itself launched from.don the Runs screen (orctrl+don New run) opens a directory picker; the chosen directory is the base a relative workflow reference on the New Run screen resolves against, and the working directory that a launched or resumed run's detached child inherits. It is process-lifetime only — there is noconfig.tomlkey and no state file, and it resets whenconductor fleetexits. It does not affectruntime.working_dir/agent.working_dir, and it is not a filter: Runs and History still show the whole fleet. Seedocs/fleet.md. -
runtime.idle_timeout_seconds/runtime.max_idle_recovery_attempts(#488) — Copilot-only knobs to tune the idle watchdog for workflows with legitimately long tool calls.idle_timeout_secondssets the time without SDK events before a session is treated as idle (default 90s);max_idle_recovery_attemptscaps the number of "please continue" prompts sent before failing (default 5;0fails on the first genuine idle without ever injecting a prompt). Seedocs/configuration.mdanddocs/workflow-syntax.md.
- The Pydantic AI dependency was narrowed from the full
pydantic-aipackage topydantic-ai-slim[anthropic,openai]. This drops the bundledpydantic_ai.mcpmodule, which Conductor replaces with its own toolset bridge, so the change is transparent to users. - The previously reserved
openai-agentsprovider 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.
- The Copilot idle watchdog no longer fires during long-running tool
calls (#488). The SDK does not guarantee any events during a tool
call —
tool.execution_progress/tool.execution_partial_resultexist in the SDK schema but are opt-in per tool, so for most tool calls nothing arrives betweentool.execution_startandtool.execution_complete— so a stale idle clock while a tool was still executing was previously indistinguishable from a genuinely stuck session — triggering a spurious "please continue" recovery prompt mid tool-call. That prompt's conversational reply then overwrote the agent's eventual structured output (response_contentis last-message-wins), turning a healthy run into a non-retryable failure. In-flight tool calls (tracked bytool_call_id) now suppress idle recovery entirely while any remain outstanding;max_session_secondsis the sole backstop for a genuinely wedged tool. Recovery-prompt and stuck-session messages also no longer misattribute the failure to a tool that has already completed —last_activity_ref's tool name is now cleared (or rolled to another still-in-flight tool) ontool.execution_completeinstead of only ever being set. The first occurrence of extended suppression during a session is logged atwarninglevel (naming the in-flight tools and themax_session_secondsbackstop); further occurrences in the same session are debug-only. - Retry classification now covers the
ModelHTTPErrorandModelAPIErrortypes pydantic-ai actually raises, so408,429and5xxresponses are retried on the Claude provider as well as the new OpenAI one. Previously they were treated as fatal. runtime.default_reasoning_effortwas silently dropped at run time for every provider and is now forwarded throughProviderRegistry.- 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.ToolfrominputSchematoinput_schemaand onmcp.types.CallToolResultfromstructuredContenttostructured_content, retaining the camelCase name as the serialization alias in both cases. The second rename failed quietly: a tool returning only structured content raisedAttributeError, which was wrapped into aRuntimeErrorthe model read as an ordinary tool failure. Conductor now reads both fields through a shared helper that tries the 2.x name and falls back to the 1.x one, preserving compatibility with both MCP 1.x and 2.x. - The Fleet Manager TUI's launch-directory picker no longer clobbers its
own prefill (#486). Textual posts a
NodeHighlightedevent for the directory tree's own root at mount, from reactive initialisation, with no user interaction involved;DirectoryPickerModalmirrored every such event into the input, silently replacing the prefilled launch directory with its parent before the user ever touched the tree. The mirror now fires only while the tree actually has focus. - The Fleet Manager no longer loses data from large event logs (#485).
The Runs, History, and run-detail screens read a run's JSONL event log
through three separate bounded windows — a 512 KiB tail, a 512 KiB head
recovery read, and an 8 MiB whole-log cap — that a long-lived or resumed run
outgrows. On a real 9.72 MB / 20,361-line log this lost the current step and
the token/cost totals, and a resumed run's second
workflow_startedfell outside the head window, so the wrong workflow topology was reported. All three windows are replaced by one streamed reader bounded only by the longest individual line, and a resumed run's totals now accumulate across generations while its status, gate, and topology reflect the current attempt. The Runs screen's ~2s poll prefilters lines by event type before parsing them, so the uncapped read is also faster than the capped one it replaces. - A run record could silently fail to be removed on Windows (#486).
remove_run_recorddeleted a record with a single unretriedunlink, andremove_run_record_for_current_processrenamed it into a quarantine path with a single unretriedrename; on Windows, a concurrent reader can make either fail with a sharing violation, leaving a stale record behind. Both paths now use the same bounded retry thatwrite_run_recordalready used for its ownos.replace. - The Copilot provider now recovers automatically when its nested runtime
process dies (#483), instead of retrying against a dead process with
a misleading "Check that copilot CLI is installed and authenticated"
error. A dead spawned runtime is now detected via
subprocess.Popen.poll()on the SDK's own child handle and via explicit recognition ofBrokenPipeError/ConnectionResetErrorat the agent-execution SDK boundary (including during idle-recovery "continue" prompts, which previously burned every recovery attempt and were reported as a stuck agent rather than a dead process). Recovery rebuilds the SDK client the next time it is needed, so the existing retry loop lands its next attempt on a fresh runtime with no change to retry-loop shape; a runtime that keeps dying without a single successful call in between fails fast after 2 consecutive restarts (a fixed, non-configurable cap — with the defaultmax_attemptsof 3, a single agent execution can only trigger 2 restarts on its own, so the cap mainly bites across agents in the same workflow) rather than looping forever, while a long-running, otherwise-healthy workflow can restart it as many times as needed. A broken connection to an externally-owned runtime (runtime_url/COPILOT_PROVIDER_RUNTIME_URL) is treated differently: it is never retried or respawned, since the orchestrator that owns that runtime is responsible for its health checks and restarts.
0.1.33 - 2026-08-18
- The Fleet Manager TUI's History screen can now resume a run by
pressing
ron a row that correlates to an on-disk checkpoint, launchingconductor resume --web-bgin the background the same way the New Run screen launches a fresh workflow. Gating is checkpoint-driven, never derived from the row's outcome — anunknownrow (no terminal event) offers Resume exactly like afailedone when a checkpoint exists for it, though this only applies when the workflow opted into periodic checkpoints (runtime.checkpoint) or failed and left a failure checkpoint behind. A currently-live run is always excluded, regardless of outcome or checkpoint — resuming a run that is still executing would make the new process adopt the originalrun_id, overwrite its run record, and interleave two processes' events into one log. Seedocs/fleet.md. - Session continuity for the
claude-agent-sdkprovider via a per-agentsession_key— executions tagged with the same key now continue one Claude session instead of each starting cold, so an investigate → check → retry loop keeps what it already read, and a later agent can inherit an earlier one's conversation by declaring the same key. The key is a static, unrendered label; sessions are scoped per working directory, since that is how theclaudeCLI stores transcripts. The map is persisted in checkpoints, so continuity survivesconductor resume, and the newsession_continuitycapability turnssession_keyagainst a provider that cannot honor it into aconductor validateerror rather than a silently dropped setting. A session the provider cannot confirm on disk logs a warning and starts fresh rather than failing the run, andconductor validaterefuses a key shared across concurrent executions. Seedocs/workflow-syntax.mdandexamples/claude-agent-sdk-session-key.yaml. - Checkpoints now persist every active provider's session map, rather than
stopping at the first provider that exposes one. A workflow mixing providers
previously kept only one map, silently dropping the others' sessions
depending on which agent happened to run first.
claude-agent-sdknamespaces its own entries, so they cannot collide with Copilot's agent-name keys in the merged map. - Fleet Manager TUI: RDP session detection turns animation off
automatically (issue #462). An RDP session (
SESSIONNAMEstartingRDP-Tcp) now disables the ~10fps animation clock by default — the same repaint that made the TUI feel laggy over that transport. SSH is deliberately not detected: it ships the ANSI byte stream for the local terminal to render (a few hundred bytes per frame), where RDP renders remotely and ships changed pixel regions, so only the latter is costly in practice.CONDUCTOR_FLEET_NO_ANIMremains the remedy for a genuinely slow SSH link and for transports with no reliable signal (VNC, Citrix, xrdp). The existingCONDUCTOR_FLEET_NO_ANIMforce-off switch still wins over detection, and a newCONDUCTOR_FLEET_ANIMforce-on switch overrides detection when the operator knows the link can take it. Any path that disables animation — explicitCONDUCTOR_FLEET_NO_ANIMor detection — now also sets Textual's ownApp.animation_leveltonone, which additionally stops Textual's built-in widget animations (e.g. the tables' smooth-scroll easing); this is a behavior change for existingCONDUCTOR_FLEET_NO_ANIMusers, not only for the new detection path. Seedocs/fleet.md.
runtime.skill_injection.max_bytesnow defaults to 160KB, up from 128KB. The bundledconductorskill has grown to ~132KB, so the old ceiling no longer sat above it: aclaudeorhermesagent enabling the shipped skill would have failed outright instead of warning, which is the opposite of what the two defaults are for. The 64KBwarn_bytesdefault is unchanged, so that combination still warns. Workflows that setmax_bytesexplicitly are unaffected.examples/wait-smoke.yamlnow caps itself attimeout_seconds: 15, up from3. It doubles as CI's--web-bglauncher smoke fixture, and a cold Windows runner spends seconds of that budget on process and step overhead — so a cap sized for the ~1s the workflow actually waits reported a slow runner as a launcher failure. The timeout path it demonstrates is unchanged; drive it with a larger--input middle_duration_ms.
- Fleet Manager TUI: the ~10fps animation tick no longer repaints the
preview pane and footer (issue #462).
RunsScreen._tickused to end by calling_update_gate_detail(), rebuilding the whole previewTextand re-evaluating the footer's key bindings ten times a second for the sake of one spinner glyph — over RDP this made the whole TUI feel laggy. The preview pane is now split into#run-preview(the gate section and progress header, rebuilt on data/selection changes only) and#run-preview-score(the flowed step chips, the only part that actually animates); the frame tick now only repaints the latter, alongside the animated table cells it already updated. Seedocs/fleet.md. claudeprovider:validate_connection()no longer fails startup when an Anthropic-compatible endpoint doesn't implementmodels.list()(issue #455). Azure AI Foundry's Anthropic endpoint, and some LiteLLM/Databricks AI Gateway configurations, answer/v1/modelswith a 404 while/v1/messages(what agents actually call) works fine — previously this made every workflow using such an endpoint fail before running a single agent. The startup probe now only fails on positive evidence of a broken setup: an unreachable host, rejected credentials (401/403), or a non-HTTP error. Any other HTTP status logs a warning naming the status code and continues, deferring credential verification to the first agent execution — the same posture thehermesprovider already documents. Seedocs/providers/claude.md.- A step with no model no longer constructs a provider just to report a
context window. Every step type emitted
agent_startedwith acontext_window_maxresolved through the provider, and the registry builds providers lazily — so await,set,script,terminate, orhuman_gatestep built an SDK client whose only possible answer wasNone. That construction runs inside the engine's timed loop, so it was charged tolimits.timeout_seconds: a provider-free wait workflow paid ~0.4s of it locally and enough on a cold Windows CI runner to time the workflow out and fail the--web-bglauncher smoke job. Provider-backed agents are unaffected — they still report the window on bothagent_startedandagent_completed. - Fleet Manager TUI: the footer now says what
enterdoes on each screen (issue #459). Every drill-down screen boundenterbut left it unlabeled, so the one key that navigates the TUI was the one key the footer never advertised — Runs opens the run detail, Run detail opens the step detail, History surfaces theconductor replaycommand, Providers expands or collapses a provider, and Registries opens that registry's workflows. The binding is also hidden whenever it would do nothing: an empty, failed, or still-loading table, or a Providers sub-row that is not a provider. Expanding a provider a second time now collapses the provider you were actually on rather than whichever row the rebuild left under the cursor. Seedocs/fleet.md. - The Pydantic AI provider (
claude) never retried on HTTP 429/5xx or transport errors (#454). pydantic-ai's Anthropic model translates the SDK's exceptions before Conductor ever sees them (a private helper,_map_api_errorsin pydantic-ai 2.x, written inline at the 1.44.0 floor) intoModelHTTPError(for an HTTP error response) andModelAPIError(for a connection/timeout failure), so neither the SDK class names nor theanthropic.APIStatusErrorcheck that_is_retryable_errorrelied on ever matched — every attempt failed fast as a non-retryable error regardless ofretry:configuration. Both translated types are now classified directly, matching the existing 429/5xx retryable set, and a server'sretry-aftervalue is recovered from__cause__(the translation drops response headers, but preserves the original SDK exception there) or from the response body. - A per-agent
retry.delay_secondslarger than the 30s provider default was silently clamped back down to 30s on both the Pydantic AI (claude) and Copilot providers, sodelay_seconds: 60produced 30s waits instead of the stated 60s. The internal backoff cap is nowmax(default_max_delay, delay_seconds), so a larger stated delay raises the cap instead of being clamped by it; existing configurations withdelay_secondsbelow the default are unaffected.
0.1.32 - 2026-08-16
-
The
--web-bglaunch gate now terminates the whole workflow process tree, not just the pid it spawned, closing the orphan a trampolinesys.executablecould leave behind (#447). Issue #444 fixed the launch gate's false-positive port conflicts but left its four failure paths terminating onlysubprocess.Popen.pid— under a trampolinesys.executable(e.g. a Windowsuv tool install, the documented install path), that pid is a re-exec shim, not the process actually running the workflow, so a launch-gate failure could kill the shim and leave the real workflow running, undiscoverable, and still burning tokens. On Windows the child is now created suspended and assigned to a fresh job object before it can run (so it cannot re-exec out of reach), withTerminateJobObjectreaching the whole tree regardless of exec depth; the job deliberately has noJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEso the tree still survives the launcher exiting, which is the entire point of--web-bg. On POSIX,os.killpgnow reaches the process group the detached child already leads. After the tree kill, a final liveness sweep independently confirms every pid the gate knew about is actually dead rather than assuming it — a survivor is now named explicitly in the error message (with aconductor status/conductor stop --portpointer) instead of the message unconditionally claiming "The background process was terminated." A run record is only removed once its pid is confirmed dead by that sweep, so a surviving orphan keeps the record that isconductor stop's only remaining handle on it. -
--web-bgno longer fails on every port with a false "Port already in use", killing a healthy run (#444). The launch gate's two identity checks both compared against the spawned process's pid (subprocess.Popen.pid), which is not always the pid of the process that ends up running the workflow: on a trampolinesys.executable(e.g. auv tool installon Windows, the documented install path) the spawned process re-execs into a different one. That made the run-record poll (stage one-and-a-half) never see its own child's record — surfacing as "did not report a run record within 15 seconds, but is still running" — and then made stage two's/api/infoprobe report every port as held by a foreign process, terminating the healthy child. The run-record poll now also accepts a record whosepiddiffers fromPopen.pidwhen the record is fresh (written at or after this launch spawned its child), and carries the record's realpidforward as the confirmed identity for stage two. APORT_CONFLICTis now only raised when that identity was confirmed; an unconfirmed mismatch degrades to the existing non-fatal "still initializing" note instead of ever being fatal. ThePID unknownwording seen alongside the conflict is fixed too — the foreign pid is now captured before the child is terminated instead of probed after, when it can no longer answer. -
The Fleet Manager TUI no longer appears to freeze while a modal is open (#448). Opening the gate options modal (
g) left the Runs screen animating underneath it — a covered screen is still composited, so its ~10fps repaints kept re-blending the modal sitting on top. Measured on one 160x45 terminal at an open gate, that was roughly 2.5x the escape sequences and ~40% more CPU than the same screen with no modal up. On a terminal that cannot absorb that stream — over SSH, in a multiplexer, on a slow emulator — keystrokes queued behind the redraw and the modal appeared frozen. The animation is now suppressed while a screen is covered and its timer paused, which also stops the Runs screen animating under the splash, run-detail, history, providers, registries, and new-run screens. The ~2s data poll is deliberately left running, so gate-entry and run-failure notifications still fire while a modal is up. The empty-fleet state also no longer pairs "no runs" with a preview pane still offeringgfor a run that had gone. -
The Fleet Manager TUI's kill confirmation prompt is no longer an empty red box (#449).
#confirm-dialoghadwidth: autowhile both of its children fell back to Textual's base1fr, and an auto-width container whose children are all1frresolves to zero — so the dialog collapsed to 0x0 and painted nothing but its border, leavingklooking like a broken no-op with no way to see what was about to be killed. The dialog now has a fixed width (capped at 90% of the terminal so it still fits a narrow one), its message scrolls instead of overflowing or being silently truncated, and the confirm/cancel hint is docked to the bottom so a long message cannot push it off screen. -
Fleet Manager TUI no longer blocks the Textual event loop on the Runs screen's ~2s poll, the run-detail screen's poll, History's initial load, or opening a run's dashboard (#437). Each screen's data load now runs in a worker thread (
asyncio.to_thread), with rendering back on the event loop, so the UI stays responsive on a large fleet or a slow filesystem (e.g. a WSL dashboard-open call that can take up to 15s). A tick arriving while the previous scan is still running is skipped rather than started alongside it, and each screen shows a brief "Loading…" line while its first result is in flight. An explicit refresh — after a kill, or after a gate is resolved — is coalesced rather than skipped, so those actions still update the table without waiting out a poll interval. -
The Fleet Manager TUI now tells you when it cannot read a run, instead of showing something that looks like success (#437 review). A run-record directory it cannot read is reported on the Runs screen rather than leaving a "Loading…" line that never resolves or a table silently frozen at its last good contents; a fleet whose summaries all fail to derive is reported as an error rather than as the "no runs — launch one" empty state, which invited launching a duplicate of a workflow that was still running; and a History read failure is reported rather than rendered as "No run history yet.", which claimed absence and, since that screen loads once, never corrected itself. A run whose summary fails to derive on one poll tick also no longer loses its notification history, which had made it re-fire its gate/failure terminal notification on the next successful tick.
0.1.31 - 2026-08-15
- Install scripts now diagnose a blocked package index instead of retrying
into a generic failure. On networks that block direct access to the public
Python package index — increasingly common on managed corporate devices —
uv tool installfails with a fetch/403/DNS-shaped error. uv has already exhausted its own retries by that point, so both scripts spent their 2s/5s/10s backoff on a failure that cannot heal, andinstall.ps1then printed file-lock advice (including a Windows Defender exclusion suggestion) that is both useless and misleading for a network-policy block. Both scripts now classify the failure, stop after the attempt that hit it, and explain the actual remedy: point uv at your organization's index withUV_DEFAULT_INDEX. They also echo the active index at install time — with any credentials in the URL redacted — so "did my override apply?" is answerable from the install log. On the blocked-index pathinstall.ps1no longer reaches its Defender advice. - Two neighbouring failures are told apart rather than blamed on the index.
uv words an unreachable git remote exactly as it words an unreachable index,
and the installer fetches Conductor itself from
git+https://github.com/...— so a blockedgithub.comwas being reported as a blocked package index, sending users to configure something that could not help. It now gets its own message. Separately, connection-level blips (connection resetand friends) still get the full retry schedule, since unlike a policy block they can genuinely heal; only a definitive block short-circuits the retries. - README: "Installing behind a proxy or private package index" — how to
install through a mirrored/proxied index with uv (
UV_DEFAULT_INDEX,uv.toml, named-index credentials, TLS inspection, proxies) and with pip/pipx, including the trap that uv does not read pip's configuration, sopip config set global.index-urlalone has no effect on the install scripts,uv tool install, orconductor update. Conductor ships no default mirror and never redirects package resolution on its own; the index is always user-supplied configuration.
--web-bg/resume --web-bg: a failed run-record write no longer kills a healthy background workflow (#435). The launch gate's run-record poll (Fleet Manager D2) used to terminate the child and fail the launch if it couldn't confirm the run record within 15 seconds, even when the child was alive and its dashboard was still reachable — treating a bookkeeping failure as a workflow failure. It now downgrades to a warning (BackgroundLaunch.run_record_written=False, surfaced via a new note pointing at the captured stderr log, and via a TUI notification from the Fleet Manager's New Run screen) and lets the launch proceed; only a child that is actually dead, or whose dashboard has gone unreachable, still fails the launch.- The
run_idformat is now defined once, in a new leaf moduleconductor.run_id(#435). Previouslyfleet/records.pyenforced a broad path-safe pattern whileengine/event_log.pyindependently enforced a narrower hex-only pattern and lowercased its input; a resumed--web-bgrun whose checkpointrun_idcontained uppercase characters could be silently folded to a different value by the event log, causing the parent's launch-gate poll to look for a key the child never wrote and kill the resumed run 15 seconds after a successful start.fleet/history.pyandfleet/retention.py's filename parsers, andfleet/records.py's own timestamp parser, now derive their run-id-matching regexes from the same shared pattern. - Install hints for optional extras now print a command that works, and
upgrades stop uninstalling the extras you have (#441). Every hint pointing
at an optional extra hardcoded
pip install 'conductor-cli[<extra>]', which cannot work on the documented install path:install.sh/install.ps1create auv toolvenv, which is not pip-managed, andconductor-cliis not published to PyPI so pip has nothing to resolve against there.conductor fleetwithout thetuiextra, and theaca/claude-agent-sdkprovider errors, now resolve the command from the detected install context —uv tool install --force '<spec>'for an install-script install,uv sync --inexact --extra <extra>for a source checkout, andpip installas the fallback, carrying the git URL you installed from when there is one so apip/pipx-from-git install resolves too. The suggested command reuses the install source recorded for your install (so a fork or a local build is not redirected upstream) and carries the extras you already have, becauseuv tool install --forcereplaces the tool's entire requirement set anduv syncis exact by default. A receipt that cannot be read is reported rather than treated as "no extras" — in the hint, and in both install scripts, which warn and carry on rather than either dropping the extras silently or refusing to run. For the same reason,install.shandinstall.ps1now read the existing install'suv-receipt.tomland rebuild the source asconductor-cli[<extras>] @ <source>, soconductor update(which drives them) no longer silently uninstalls[tui]or[aca]on upgrade — it also names the extras it found before you commit. New--extras <a,b>/CONDUCTOR_INSTALL_EXTRASadds an extra during an install or upgrade (rejecting one this package does not declare, which uv would otherwise accept with a warning and a zero exit status), and--no-preserve-extras/CONDUCTOR_INSTALL_NO_PRESERVE_EXTRASdrops back to a bare install. - Fleet Manager History no longer accumulates an entire retained event log
into memory to build one entry (#436).
_read_full_lognow streams parsed events one at a time instead of materializing them into a list before scanning, so building a History entry from a large*.events.jsonlfile no longer holds the whole parsed log in memory at once.
0.1.30 - 2026-08-14
- Fleet Manager:
conductor stop,conductor fleet list, and a new interactiveconductor fleetTUI now discover every run, not just--web-bgones (#431). Previously only--web-bgwrote a discoverable (port-keyed.pid) record, so a plainconductor runorconductor run --webprocess was invisible toconductor stopand had to be killed by hand. Every run path now writes arun_id-keyed JSON record to~/.conductor/runs/<run_id>.jsondescribing its mode (fg/fg-web/bg), PID, workflow path, and dashboard port (when it has one);stop,fleet list, and the TUI all read from this same store. The legacy port-keyed.pidfile is still read (and cleaned up) for a still-running pre-upgrade process, but is no longer written by any current code path. Behavior change: stopping a foreground run (modefg/fg-web— anything holding a terminal) now requires interactive confirmation, since a plainSIGTERMdiscards in-flight progress unless periodic checkpoints are enabled for that run; a background-only fleet is unaffected. Use--yes/-yto skip the prompt (e.g. scripts, CI); a non-interactivestdinwithout--yesrefuses to proceed rather than silently defaulting to "yes".stopalso gained--run-id, the only selector that can target a foreground run with no dashboard port to match on. Seedocs/cli-reference.md. conductor fleet(#431) — an optional interactive Textual TUI (pip install 'conductor-cli[tui]') for monitoring, managing, and launching Conductor runs across dedicated screens: Runs (home, ~2s-polled, sorted by recency), Run detail (per-agent topology and timings, not a DAG), Providers (collapsed-by-default provider/model diagnostics, reusingproviders/diagnostics.py), Registries (registries → workflows → inputs), New Run (form generated from a workflow's declaredinput:, launches via the sameconductor run --web-bgpath the CLI uses), and History (every retained run regardless of outcome, bounded by retention plus an independent 200-entry display cap, delegating replay toconductor replay <log>rather than re-implementing it). A human gate is displayed as a persistent badge for every run mode; it can additionally be resolved from the TUI (g) for any run with a dashboard port (fg-web/bg) via the existingconductor gate respondHTTP path — a plain foreground run's gate is display-only (its PID is shown) since its blocking prompt thread cannot be reached remotely. A terminal bell / OSC 9 notification fires once per transition intoat-gateor a failure.conductor fleet listandconductor fleet pruneneed no optional dependency; only the bare, no-subcommandconductor fleet(which launches the TUI) requires thetuiextra. Seedocs/fleet.md.~/.conductor/config.toml(#431) — a new machine-wide, read-only-in-v1 settings file (src/conductor/settings.py), read with stdlibtomlliband honoring$CONDUCTOR_HOMEthe same wayregistries.tomldoes. Currently controls[fleet.retention]: an opportunistic sweep (enabled = trueby default,keep_last = 200) that bounds the otherwise-unbounded$TMPDIR/conductor/directory of event logs at the start of everyconductor run/resume. Never deletes thecheckpoints/subdirectory or an event log a live/resuming run still references.conductor fleet pruneis the explicit manual entry point (with--keep-last/--dry-run) and always works regardless of theenabledsetting. A missing file is normal (every setting defaults cleanly); a malformed file only breaks an explicit reader (fleet prunewith no--keep-lastoverride) — neverconductor run/resume, which swallow a settings load failure and just skip the feature it configures. Seedocs/configuration.md.
workflow_startednow records the run's resolvedinputs(#431). Two runs of the same workflow are otherwise indistinguishable in a listing. The values are written to the run's JSONL event log, which is also read byconductor replayand the dashboard.
conductor stopagainst a foreground run is no longer a silent no-op (#431). With the interactive keyboard listener active, theSIGTERMhandler delegated to the previous disposition only when it was callable — and in an unmodified processsignal.getsignal(SIGTERM)returnsSIG_DFL, anIntEnummember that is not callable, so the signal fell through and was swallowed entirely: the process survived and kept running. The handler now restores the default disposition and re-raises against itself. An inheritedSIG_IGNis honoured rather than converted into a termination.- A
questionsnode no longer leaves the run parked at an already-answered gate (#431). A questions node reusesgate_presentedbut never emitted the matchinggate_resolved, so every consumer of the event stream — the web dashboard as well as the Fleet Manager — held a gate that never closed for the remainder of the run.
0.1.29 - 2026-08-13
- Hardened the web dashboard's HTTP/WebSocket surface (#397). Every
mutating route (
POST /api/stop,/api/kill,/api/resume,/api/gate-respond,/api/guidance) and the/wshandshake now require a per-run token by default — previously the only protection was the optionalCONDUCTOR_GATE_TOKENenv var, and requests were unauthenticated when it was unset. The token is minted automatically per run and discoverable byconductor gate respond/guide/stopvia a new0600file (POSIX; on Windows the mode bits are not honoured and the file relies on the user-profile ACL instead — see #425) at~/.conductor/runs/dashboard-<port>.token;CONDUCTOR_GATE_TOKENstill overrides it when set. A new pure-ASGIOriginHostGuardmiddleware also validates theHostand (when present)Originheaders on every HTTP and WebSocket request, closing the DNS-rebinding and CSRF-from-another-open-page angles;CONDUCTOR_WEB_ALLOW_ORIGINS(comma-separated origins) extends the allowlist for local dev servers. Breaking for external API callers: every mutating route now also requiresContent-Type: application/json(415 otherwise), including the previously bodyless control POSTs, and a request whoseHost/Origindoesn't match the bound dashboard is rejected with 403 regardless of token. Read-only routes (/api/state,/api/info,/api/logs,/api/gate-status,/api/files/*, and the replay dashboard) remain unauthenticated, protected by Origin/Host only. - Hardened the ACA agent runner's transport surface (#396). The
experimental
acaprovider's in-container runner previously relied entirely on the Azure session-gateway network boundary; it now adds four independent layers, none individually load-bearing. The runner binds127.0.0.1by default (the shipped container image setsACA_RUNNER_HOST=0.0.0.0explicitly, so a deployed pool is unaffected — only a runner started by hand changes behaviour). An opt-in transport token,ACA_RUNNER_AUTH_TOKEN, makes/executerequire a matchingX-Conductor-Runner-Tokenheader — checked before the inner Copilot provider is constructed — and401otherwise; the host sends the header automatically when the same value is set on its side.GET /healthstays unauthenticated (the image's ownHEALTHCHECKsends no header) but now reportsauth_requiredandauth_token_present, letting the host warn when a gateway is silently stripping the header or when only one side has a token configured. The runner also rejects anyinner_provider_settingskey outsidebase_url/api_key/bearer_token/github_token, closing offruntime_urlandheadersinjection, andACA_RUNNER_ALLOWED_BASE_URLSoptionally restricts which BYOKbase_urlvalues are accepted. Seedocs/providers/aca.md#security.
-
A pathological gate or dialog prompt no longer stalls the event loop (#395).
linkify_markdown— which runs on human-gate prompts, dialog turns, and rendered agent prompts — degraded to quadratic time on inputs containing long unterminated runs of backticks/tildes or[characters, so agent-generated text could freeze every concurrent agent sharing the loop. The fenced-code opener no longer backtracks character-by-character, and existing-markdown-link detection is now a linear single-pass scanner (fuzz-verified equivalent to the regex it replaces). A defensive 256K character cap skips linkification entirely on anything larger — whitespace is still normalized — so a future pathological shape degrades gracefully rather than hanging. -
Token cost is no longer massively overstated for cached, tool-calling agents.
AgentOutput.input_tokensis the whole prompt and already containscache_read_tokens/cache_write_tokens, butcalculate_costbilled all four buckets additively — charging every cached token at the full input rate and again at the cache rate (11x onclaude-sonnet-5). Because a long agentic loop re-reads almost its entire prompt from cache on every turn, the error compounded across turns: a real run reporting $51.08 actually cost about $8. A cached bucket is now subtracted from the input bucket before the input rate is applied, so each physical token is priced exactly once — the same treatmentgenai-pricesuses, including its rule that a bucket is only subtracted when a rate exists to charge it at (a0.0cache rate in the table means "no published rate", not "free"). Cost figures on the dashboard, the CLI summary,agent_completedevents and the JSONL event log all drop accordingly; no workflow config changes. The Claude Agent SDK provider, whose Anthropic-shaped usage dict reports cached tokens outsideinput_tokens, now folds them in and reports both cache buckets, so cached tokens there are billed at the cache rate instead of not being billed at all — note this also makes that provider's reportedinput_tokens/tokens_usedcounts cache-inclusive, matching Copilot, so token totals rise even as cost falls.If you set
limits.budget_usd: existing values were calibrated against the inflated figures and now permit correspondingly more real spend. Review them, particularly underbudget_mode: enforce. -
claude-opus-5and the dotted Claude 4.5 names are no longer unpriced.DEFAULT_PRICINGhad noclaude-opus-5entry, andget_pricing's versioned-suffix fallback only extends a key with a-delimiter — so the SDK-advertisedclaude-haiku-4.5never matched the dashedclaude-haiku-4-5entry either. Those models fell back toNoneand were reported as unpriced whenever the provider's live pricing hook was unavailable (an older Copilot SDK, or a non-Copilot provider). Addedclaude-opus-5,claude-opus-4.5,claude-sonnet-4.5andclaude-haiku-4.5at the published Anthropic rates. Both spellings are kept: the dashed keys are what price the date-suffixed Anthropic ids (claude-haiku-4-5-20251001). -
gpt-5.6-sol,gpt-5.6-terra, andgpt-5.6-lunaare no longer unpriced (#386). Added toDEFAULT_PRICINGas exact keys at the existing GPT-5.x family rate ($2.00 in / $8.00 out per million tokens) — exact keys rather than agpt-5.6prefix key so the three resolve silently instead of throughget_pricing's fuzzy-match warning path.grok-4.5,gemini-3.6-flash,mai-code-1.1-flash, andmai-code-1-flash-pickerremain deliberately unpriced in the static table pending a published rate; an invented rate would print as a confident cost, which is worse than the honest(N unpriced)marker. The live provider-pricing hook prices any model whose SDK metadata carriesbilling.token_prices(verified forclaude-opus-5in #418, on Copilot SDK>=1.0.9— already the hard floor pinned inpyproject.toml) — the static-table gap only matters when that hook is unavailable or the model's metadata lacks a rate.
conductor doctor --modelsnow shows per-model pricing (#386). The Models detail table gainedInput $/Mtok,Output $/Mtok, andPricingcolumns — the last distinguishingprovider(liveget_model_pricinghook),table(staticDEFAULT_PRICINGfallback), andnone(genuinely unpriced) so "why is my run unpriced" is answerable with one read-only command (pricing resolution adds no new network round-trip — the Copilot SDK memoizeslist_models()for the process).--jsongains matchinginput_per_mtok/output_per_mtok/pricing_sourcefields on each model object.
0.1.28 - 2026-08-12
-
Mid-run guidance for
--weband--web-bgruns (#400). The dashboard previously offered only Stop, Resume, and Kill — there was no way to correct a run's course without stopping it first.conductor guide --text "..."(auto-discovering the dashboard port) and a dashboard Guide button both POST to a newPOST /api/guidanceendpoint, which feeds aGuidanceChannelthe engine drains at the next step boundary (agents, parallel groups, for-each groups, scripts, sets, and waits alike) or immediately if an agent is currently paused, in which case it resumes with the guidance applied — reusing a Copilot follow-up on the same session when one is available. The TTY Esc/Ctrl+G interrupt path now goes through the sameadd_user_guidanceentry point, so that guidance is visible in the dashboard and JSONL log too, and parallel/for-each group members now receive the current guidance section (previously always omitted).resume --guidance "..."(repeatable) applies guidance to the restored context before the resumed agent runs. Protected by the sameCONDUCTOR_GATE_TOKENasconductor gate respondwhen configured. -
conductor status— see what is running without stopping it (#384).conductor stopwith no arguments lists background workflows, but stops one when exactly one is running, so the natural "what's running?" reflex was destructive precisely when there was a single run to lose.statusnever terminates anything and never removes a PID file, so a run stays discoverable even when its liveness cannot be confirmed. It prints each run's dashboard URL, which is otherwise unrecoverable once the launching terminal is gone, and--jsonmakes it scriptable. A malformed PID file is skipped with a warning rather than taking down the listing. -
Git-backed plugin sources (#380).
runtime.pluginsalone resolves against machine state — an installed plugin name, or a path — so a workflow shared with a teammate still needed "first install these plugins" in a README, and a teammate who skipped that step got a hard error rather than a working run.runtime.plugin_sourcesmaps a marketplace name to where it comes from (owner/repo#v1.4.0, any http/https/ssh orgit@host:pathremote, or a local path), andplugins:entries reference it asprs@acme. The split follows the Copilot CLI's own settings, which separateextraKnownMarketplacesfromenabledPlugins— eleven plugins commonly come from one repository, so inlining a URL per entry would either clone it eleven times or silently pick one of eleven refs. The load-bearing property is thatprs@acmemeans the same thing whether the marketplace was declared, installed via a CLI, or is a local directory: a declared source registers its name into the same resolution table the installed roots populate, so git feeds resolution rather than adding a second code path. It also gives the ambiguity error added in #378 a second remedy — qualifygitasgit@acmeinstead of falling back to a path. Both repository shapes are handled: amarketplace.jsoncatalog or aplugin.jsonsingle plugin, with aplugin:key for a repository that is both. There is no lockfile — the YAML is the lock. A ref that is a full 40-character SHA is pinned and fetched once; a tag, branch, or absent ref floats and is re-resolved every run, matching how workflow registries already behave.conductor runacquires sources up front and in parallel;conductor plugin fetchprimes the cache as its own step, which is what keepsconductor validateoff the network entirely;conductor plugin listreports what a run would load, including the component counts that make a change in what a plugin ships visible. An unreachable remote with a warm cache warns and reuses the checkout, so offline runs keep working. Checkouts are cached under$CONDUCTOR_HOME/cache/plugins/, keyed by resolved commit. Cloning shells out togit, so existing SSH keys and credential helpers apply and self-hosted forges work.Sources are resolved one at a time, so a source that is unfetched or broken costs its own diagnostic rather than the report for every healthy source beside it. The two are distinguished: an unfetched source is a warning naming
conductor plugin fetch, sinceconductor runheals it, while a source that is itself wrong — a path that does not exist, apath:that escapes the checkout, an unparseable catalog — is an error, because no amount of fetching fixes it. A declared source that shadows a same-named installed marketplace is reported, since the two can ship different subagents or a different MCP server. Seeexamples/plugin-sources.yamland the Plugins section ofdocs/workflow-syntax.md. -
Output field constraints —
enum,pattern,minimum/maximum,minLength/maxLength,required,nullable(#372). Anoutput:field could declare a type and nothing more, so "verdict is one of three values" or "score is 0-100" lived in the prompt, where it was a suggestion rather than a contract. The eight new keywords are emitted into the schema each provider shows its model and enforced when the response comes back, and because a violation raises the sameValidationErrora type mismatch does, it lands inside the existing in-session recovery loop — the model gets a chance to correct itself before the workflow fails. Constraints are checked recursively, so they hold inside object properties and array items too.Illegal combinations are rejected at load time rather than at run time:
patternon a number, anenumwhose members do not match the declared type,minLengthabovemaxLength, a regex that does not compile. Unknown keys are rejected as well, so a misspelledminlengthfails validation instead of quietly leaving the field unconstrained.required: falseis allowed only inside object properties — a root-level output field cannot be optional.Two things worth knowing when using them.
patternruns under a one-second deadline on are-compatible engine, because model output is untrusted input and a backtracking pattern would otherwise stall the event loop and every agent sharing it; an exceeded deadline is a validation failure, not a hang. And templates render withStrictUndefined, so anullablefield that came back null renders asNoneand an omitted optional property raises — guard both withis not none/is defined, asexamples/output-constraints.yamlshows. Seedocs/workflow-syntax.md(Field Constraints). -
Plugins as the unit of opt-in (#378). Conductor loaded a plugin's
skills/and dropped everything else it shipped. That is a problem because a plugin's parts are written to work together: itsSKILL.mdroutinely tells the agent to hand work toprs:code-reviewer, or to call anadoMCP tool. The skill loaded, the agent read those instructions, reached for a subagent that was never registered — and said nothing.runtime.plugins(and per-agentplugins:) now opts into the whole unit: skills,agents/*.agent.mdsubagents, and declared MCP servers. Entries take a string shorthand or an object with per-component switches (skills,agents,mcp), all defaulting on, because defaulting one off would recreate the partial load the feature exists to fix. An entry is an installed plugin name or a path, classified by the same syntactic ruleskills:uses; an uninstalled name errors naming where it looked, and an ambiguous one errors rather than picking a winner. Conductor deconstructs a plugin rather than handing its root to the SDK: both SDKs' whole-plugin surfaces are all-or-nothing, and on Copilot hiding an MCP tool from the model does not stop its server subprocess launching with the user's credentials — somcp: falsebuilt that way would be a guarantee that isn't one. Deconstructed, a plugin's MCP servers also pick up the sameruntime.tool_outputlimits, dashboard tool events, and credential/${VAR}resolution as a workflow-declared server. Supported oncopilotandclaude-agent-sdk;claude,hermesandacarejectplugins:at validation time, since injecting text into a prompt cannot produce a subagent or an MCP server.conductor validateprints what each plugin contributes, including every subagent by name, so a change in what a plugin ships is visible before the run rather than during it. Seeexamples/plugins.yamland the Plugins section ofdocs/workflow-syntax.md. -
Copilot-convention plugin manifests are recognised (#378).
.github/plugin/plugin.jsonnow resolves alongside.claude-plugin/plugin.json. Both have always worked at runtime, so recognising only the latter was Conductor's own gap — on an ordinary machine it stranded 12 of 13 installed plugins, which onclaude-agent-sdkmeant a packaged skill was rejected outright and oncopilotmeant it was silently demoted to a bare directory. -
type: questions— ask a human a set of questions in one step (#376).human_gatehandles a single decision; asking N questions previously meant hand-rolling a gate that loops back through asetstep accumulating a string transcript. That loop cannot support going back — a workflow step cannot be un-executed, and a concatenated transcript has no addressable per-question answer to overwrite — and it costs two engine iterations per question. Aquestionsnode holds the cursor and answers internally, so the whole set costs one iteration and answers land in a keyed dict where revisiting question 3 overwritesanswers.q3. Questions come from an inlinequestions:list or asource:dotted path; entries may be plain strings or objects withchoices, so an agent already emittingarray of stringmigrates unchanged while gaining candidate answers is a backward-compatible upgrade. Supports back/skip/skip-all/abort,required, per-questiondefaults, a closing review, and partial answers that survive a checkpoint.--skip-gatesnever selects a suggested answer — those come from the agent, so recording one would feed invented input back as though a human gave it. Seeexamples/questions.yamland the Questions section ofdocs/workflow-syntax.md. -
Opt-in multi-line text for human gates —
GateOption.multiline(defaultfalse, so existing gates are unchanged). The terminal reads until a lone.or EOF; the dashboard renders a textarea where Enter inserts a newline and Ctrl/Cmd+Enter submits. Previously a multi-paragraph answer to aprompt_forinput was silently truncated at the first newline.
skill_discovery.sources: [plugins](#378). The source scanned every installed plugin'sskills/directory — reaching into a plugin and taking exactly one of the three things it ships, which is the bugruntime.pluginsfixes rather than a feature with a gap. It was also wrong more often than it looked: of 13 plugins on an ordinary machine, 3 loaded their instructions without the subagents those instructions dispatch to, and the 3 most plugin-like — MCP and subagent toolkits with noskills/at all — were never discovered by it. Nothing distinguished the working set from the broken set at authoring time, on a machine the workflow author may not even be using.personalandprojectare unchanged. Replacesources: [plugins]withruntime.plugins, which brings the whole plugin and, unlike a scan, reproduces on another machine.
-
--web-bgno longer reports success and prints a URL for workflows that never actually started (#410). The launcher's readiness check used to trust a bare TCP connect: the moment anything accepted a connection on the dashboard port, it wrote the PID file, printed the URL, and exited 0 — even for a workflow that failedload_configmoments later. Two changes close this: (1)WebDashboard.start()(which binds the port) now runs afterload_configsucceeds inrun_workflow_async, so aConfigurationErrorfrom a broken workflow never binds a port in the first place; (2)_finalize_background_launchnow confirms the workflow actually started, not just that a socket answered._wait_for_serverchecks the child's exit status on every iteration of its connect loop, so a dead child is detected in well under a second instead of after the full 15s timeout; a stage-two probe then pollsGET /api/info(the same identity endpointconductor stopalready uses) for up to 30s (CONDUCTOR_WEB_BG_START_TIMEOUT,0disables it) until it reports aworkflow_startedevent, exiting 1 with the exit code and a tail of the captured stderr log if the child dies first, or naming the conflicting PID if the port turns out to be held by an unrelated process. The PID file is written as soon as the port opens — before this second wait — so a slow-starting run stays visible toconductor status/stopthroughout; if the child then dies, the entry is removed. Passing the 30s deadline with the child still alive is not treated as a failure — the URL is still printed, alongside a note that the workflow hasn't reported starting yet. -
Live provider pricing works again (#386). Every Copilot model was being costed from the static
DEFAULT_PRICINGtable instead of the live rates the SDK reports, and models absent from that table reported no cost at all.CopilotProvider.get_model_pricingreadsbilling.token_prices, andgithub-copilot-sdk1.0.1 — the version the lock pinned — parsed themodels.listresponse with a hand-writtenclient.ModelBillingthat declared onlymultiplierand discarded thetokenPriceswire field. The field never left the API and is still modelled in the SDK's generated types; only the client dataclass dropped it. The hook therefore returnedNonefor every model, so the resolution chain #265 built (workflow override → provider hook → static table → unpriced) ran permanently on its fallback. No per-token rates were invented for the missing models; with the hook alive they price from the SDK.The field was restored in SDK 1.0.7; the floor moves to
>=1.0.9, the version tested here. Moving the floor rather than only the lock is the point — the old>=1.0.0was satisfied by 1.0.1, so an existing environment kept dead pricing while reporting a healthy dependency. 1.0.9 also splits the cached-token rate into separate read and write prices and deprecates the singlecache_pricethe hook read, which would have silently priced cache reads at $0.00, and it ships a pure-Python wheel that fetches the CLI binary on first use instead of bundling it per platform._default_permission_handlerno longer forwardsapprove_all's result blindly. That helper stopped being unconditional: it abstains withPermissionNoResultwhen the runtime marks a requestmanaged_approval_required, and raises when managed settings are enabled. Conductor is the only connected client, so an abstention is never answered — the CLI blocks on a pending permission request until idle recovery gives up minutes later and reports a timeout that blames the network. Both cases now decline explicitly and say why. Declining rather than approving is deliberate: managed approval is a policy control, and overriding it would turn a hang into a bypass.The existing hook tests built their models from
SimpleNamespace, so they asserted what Conductor does with a billing object rather than whether the SDK still supplies one, and stayed green throughout. Tests now build the model through the SDK's ownModelInfo.from_dict, so the next SDK release that stops carrying the field fails the build instead of quietly reverting every run to static pricing, and the permission handler has behavioural coverage for the first time. -
Plugin checkouts from a
file://source no longer land outside the plugin cache on Windows. The cache key is derived from the URL's path segments, but the splitter only knew/, so a native Windows path arrived as a single segment with its backslashes intact — and the key kept them, putting the checkout at a drive-absolute location rather than under the cache root, which is the same escape the..check exists to prevent. Two further problems sat behind it: a drive colon made an owner ofC:_srcread as a drive (or, in the middle of a name, as an NTFS alternate data stream), and flattening a deep path into one segment produced a directory name long enough thatgitrefused to create.gitinside it. Separators are now folded, the characters that change a path's meaning on Windows are substituted, and an over-long segment is replaced by a digest of itself — on every platform, so one workflow file resolves to the same cache layout wherever it runs. -
Two sources resolving to the same commit no longer fail the whole fetch on Windows. Publishing a completed checkout tolerates losing the race to a concurrent fetch, but recognised only the POSIX errnos for "destination already exists"; Windows reports that as
ERROR_ACCESS_DENIED, so the tolerance never applied and the second source raised. Safe to accept because the readiness sentinel is written after publishing: a winner that died mid-clone leaves no sentinel, so the tree is re-fetched rather than read half-written. -
A local path is recognised the same way on every platform —
_is_local_pathaskedpathlib.Path, which is the running platform's flavour, so a POSIX absolute path such as/srv/pluginswas refused as an unrecognised source on Windows. Both conventions are now consulted. -
Registry names are validated before they can corrupt the config — a name containing a quote, a space,
=or#was accepted, written intoregistries.tomlas an unescaped table key, and then failed to parse. Sinceregistry add,removeandgetall load the config first, the user could not remove the entry that broke it and every unrelated registry went down with it. Names are now restricted to letters, digits,.,_and-, which also keeps them legal as cache directory names on Windows, and the table key is quoted so a dotted name stays one registry instead of becoming a nested table. -
conductor doctorno longer reports a missing Claude CLI on Windows — the CLI probe dropped five~-anchored fallback locations on Windows, including~/.claude/local/claudewhere Claude Code's own installer puts it, sovalidate_connection()returned False for a CLI the SDK would find and run. Only/usr/local/bin/claudeis now skipped there: it is rooted but driveless, so it resolves against the current drive, which any unprivileged local user can write to. -
Registry TOML values are escaped — a registry whose source or type contained a quote or a backslash produced a file that could not be re-read.
-
Dashboard context-window bar no longer reports cumulative input tokens as a false red at >100% of the cap (#412). The bar reused
AgentOutput.input_tokens— a billing total summed across every API call in an agent's execution — as a context measurement, so a multi-turn tool-calling agent (or a Copilot parse-recovery retry) could report more tokens than the model's context window physically allows. A newAgentOutput.last_call_input_tokensfield carries only the prompt size of the most recent single API call, populated by every provider, and the engine now sourcescontext_window_usedfrom it instead. When a provider cannot isolate one call's prompt size, the field isNoneand the dashboard hides the bar rather than showing a misleading number; aused > maxpair (impossible for one real API call) is dropped toNoneentirely, logged at debug on every occurrence and at warning once per run (nothing reads debug logs in production), since either the usage figure or the looked-up cap is untrustworthy. Riding along:copilot.py'sassistant.usagehandler previously overwrote its running token counts on every event instead of summing them, so a 20-turn tool-calling agent's cost was billed only for its final API call — this under-report is fixed at the same time, since fixing it alone (without the new field) would have made the context bar report the sum of every turn rather than just the last, making the original defect worse. The event's dedup guard (which prevents a repeatedassistant.usageevent from double-billing the same API call) keys onapi_call_id, falling back toprovider_call_idthenservice_request_idwhen the SDK omits it, since all three are independently optional. Cost figures for multi-turn Copilot agents will rise as a result; acost.budget_usdtuned against the old under-reported total may now trip its limit sooner. -
conductor stopno longer kills the run it is executing inside (#399). An agent smoke-testingconductor stopfrom its own workflow'sbashtool inherited that workflow's background environment and terminated itself — the process printed "Stopped" and was killed by what it printed.stopnow identifies the run it is executing inside viaCONDUCTOR_RUN_ID(set on every--web-bgchild and inherited by descendants), the legacyCONDUCTOR_WEB_BG/CONDUCTOR_WEB_PORTpair (for PID files predating #411'srun_idfield), and POSIX process ancestry, and excludes it from targeting by default:--allnow means "stop all other runs", the no-flag auto-stop skips it, and--port <your own port>is refused (exit1, naming--allow-selfas the remedy). If only your own run is alive,stop/stop --allprint a refusal and exit0rather than erroring, since nothing named was declined. Pass--allow-selfto restore the previous targeting exactly; a yellow warning is printed whenever it actually causes your own run to be signalled. Process-ancestry detection is POSIX-only — Windows relies on the env-var signals alone. -
A pricing hook that silently prices nothing is now reported (#386). #265 warns when the provider pricing hook raises; the companion case — a hook that never raises and returns
Nonefor everything — looked identical to "these models are simply unpriced", so live pricing could be dead for a whole run with no symptom beyond newer models showing up as unpriced. The verdict is drawn once when the run ends — however it ends, so a run that dies part way still reports it, which is when a partial cost total most needs the caveat — and is emitted as apricing_hook_silentevent as well as a log line, so it reaches the event log and the console rather than only unattributed stderr. The run summary gainsusage.live_pricing_degradedand the cost breakdown prints a matching caveat, because a model priced from the static table still reports a confident cost and would otherwise carry no qualification. Providers that do not implement the hook are excluded: returningNoneis the documented default, so counting them accused four of the five providers of a broken SDK for behaving correctly. -
conductor stopnow confirms the process actually stopped, and never stops the wrong one (#344).stopsent one signal and reported success without checking, so a workflow that ignored it was reported as stopped and its PID file deleted — leaving a live run untracked, invisible tostop, and holding its port. Termination is now a ladder (ask the dashboard to cancel, then signal, then force-terminate), each rung confirmed before the next, and the PID file is removed only once the process is confirmed gone. Every PID-directed rung is gated on the dashboard confirming its own PID, because between a PID file being written andstopreading it the OS may have recycled that PID onto an unrelated process.--forceoverrides uncertainty only: a positive identity mismatch blocks every rung, force included. PID files are written atomically, so a concurrentstopcan no longer read a half-written file and deregister a live run, and the reader logs before deleting anything it cannot parse.--forcecan clear an entry whose liveness cannot be probed, which would otherwise wedgestop --allat exit 2 permanently (#166). -
Bracketed text no longer crashes or corrupts CLI output (#406). The same defect as #382, which #387 fixed only in
cli/run.py.conductor validatedied with an unhandledMarkupErrortraceback on a workflow whosename:contained[/bold], and silently deleted the token when it contained[dim]. The quiet half is the more damaging one: a listing that drops part of a name looks like it worked. Rich treats a bracketed token as a style tag when its first character is lowercase,#,/or@, so[0]is fine,[task1]disappears, and[/etc/x]raises — andstyle=does not turn parsing off, which is what made the earlier fix look complete.Two consequences shipped unnoticed. Every for-each iteration's verbose panel read the same, because the engine qualifies a member's name as
<agent>[<key>]so interleaved output can be attributed to one iteration, and akey_by:key oftask1erased exactly that identity — while a key starting with/, whichkey_by:over paths or URLs produces, killed the run from a logging call. This needed no flags: verbose and full mode both default on. Separately,conductor status(#389) andconductor plugin list(#398) were written against the unfixed pattern in files #387 never touched, and #398 made these strings third-party rather than the author's own YAML, since plugin, marketplace, skill and subagent names are now read out of git-cloned repositories.Rather than escape ~450 call sites, the default is inverted: every console is built by the new
conductor.console.make_console()withmarkup=False, so a plain string is literal unless it asks to be styled, and conductor's own styling goes throughstyled("<template>", value), which parses the template but inserts values verbatim and byte-exact.Paneltitles andPromptprompts are handled separately because rich parses those regardless of the console setting — that is the trap that left #387 incomplete one line from the code it changed.rich.markup.escapeis no longer used anywhere: it cannot round-trip a value containing a backslash before a bracket, so an ordinary regex came out mangled. Eight static guards now read the source and fail with file:line if a new call site reintroduces any of these shapes — including aTextflattened back into an f-string, which is how the defect kept coming back, and unescaped brackets intyperhelp text, which had silently costconductor run --helpthe whole[@registry][@version]syntax. -
conductor status --jsonno longer ships two permanently dead fields (#404).--web-bg's launcher wrote every PID file'srun_idempty and itslog_filewas a promise it could never keep — the JSONL path is derived inside the child byEventLogSubscriber, after the PID file is already written.write_pid_filenow records the launch's actualrun_idandstderr_log/stdout_log(replacinglog_file, which the parent legitimately knows) — the same three artefacts_finalize_background_launchalready had in scope but never threaded through.run_idis the join key to the run'sconductor-<name>-<ts>-<run_id>.events.jsonl, so a populated value makes that file findable by glob without storing a path the parent would otherwise have to guess.conductor resume --web-bggoes further: it now resolves the checkpoint exactly as the resumed child does (--fromfirst, else the latest checkpoint for the workflow) and adopts itsrun_idfor the whole launch, rather than minting a fresh one that matches neither the child'sEventLogSubscriber(which reuses the checkpoint's id whenever the original JSONL still exists) nor the events log filename. A checkpoint with a missing or malformedrun_idfalls back to a fresh id rather than failing the launch.conductor status --jsonalso now emitsnullfor an absent/emptyrun_id/stderr_log/stdout_loginstead of"", so a PID file predating this fix is distinguishable from one that legitimately has no run id.conductor statusis unreleased, so thelog_file→stderr_log/stdout_logrename costs no released contract. -
conductor status's dashboard URL no longer gets cropped at a default 80-column terminal (#405). At that width, theDashboardcolumn — the one field the command exists to surface — was the one rich elided, leavinghttp://127.0.0.1:…reconstructable only by hand from thePortcolumn. Both theStartedandDashboardcolumns now fold onto a second line instead of cropping — a folded value is complete and readable, a cropped one is unrecoverable from the output.Startedalso renders to minute precision in UTC (2026-08-11 12:48Z, down from a 32-character microsecond-precision timestamp), leaving more room forWorkflowbefore folding is ever needed. The table is a glance-at listing, not an audit log, so--jsonkeeps reporting the exact recorded timestamp untouched — only the human-readable rendering changed._print_running_listis shared withconductor stop, so its listing gets the shorter timestamp too. The test fixture that let this through built its own PID-file JSON by hand with a 19-character naivestarted_at, well short of production's 32-character value — it now goes through the realwrite_pid_file, so the widths under test match the widths production writes. -
JSON result output no longer crashes on a legacy Windows stdout (#342). On a
cp1252console,conductor runexited non-zero withUnicodeEncodeErrorafter the workflow had already succeeded, having written a truncated document callers could not parse.json.dumpsemits ASCII by default, but rich'sprint_jsonre-parses and re-serialises withensure_ascii=Falseimmediately before the write, restoring the character it had escaped. Every JSON sink now passesensure_ascii=True. Results carry\uXXXXescapes on all platforms as a result, which is valid JSON and decodes identically.conductor doctor's default table output is unaffected by this change and still fails on such a console (#401). -
Agent text containing bracketed tokens no longer kills a run (#382). A step whose output contained ordinary technical prose such as
{provider}/{type}[/{nestedType}...]/readwas parsed by rich as a closing markup tag, raisingMarkupErrorand ending the workflow — unresumably, since the crash happened while rendering rather than while running. Every console sink that renders agent-supplied text now passes it asrich.text.Textrather than interpolating it into markup, and the file-log console disables markup entirely.style=does not turn markup parsing off, which is what hid three of the five sinks; two of those were reachable on a bareconductor runwith no flags. Opening tags such as[bold]were the quieter half of the same bug: rich consumed them without raising and the text simply disappeared. -
Non-ASCII workflow inputs are shown literally in verbose output (#391). The verbose "Workflow Inputs" panel serialised inputs with
json.dumps' defaultensure_ascii=True, so a Cyrillic, CJK or emoji input was displayed as\uXXXXescapes rather than the text the user typed. Every other JSON display path in the repo already passedensure_ascii=False(#356); this was the last one that did not. Machine-readable JSON results are unaffected and still ASCII-escaped, which is what keeps them safe on a legacy Windows console (#342). -
Structured
runtime.providerforname: claudeno longer drops a YAML-declaredapi_key— the schema acceptedapi_key(alongsidebase_urlandauth_token) but the provider factory silently discarded it, so only theANTHROPIC_API_KEYenv var ever reached the Anthropic client. The factory now forwards it. Two credential-semantics fixes ride along so the documented behavior is what the code does: the Claude provider's model path now resolves credentials as a unit like the Anthropic SDK does — setting either credential in YAML suppresses bothANTHROPIC_API_KEYandANTHROPIC_AUTH_TOKEN, where previously a YAMLauth_tokenstill let an ambientANTHROPIC_API_KEYride along and the SDK sent bothX-Api-KeyandAuthorization: Bearerheaders to whateverbase_urlpointed at (a credential leak against gateway endpoints). And when both credentials are set, Conductor now logs a warning naming that behavior instead of shipping both headers silently — the same parity warning the Copilot provider already logs forapi_key+bearer_token. New example:examples/claude-custom-endpoint.yaml; seedocs/providers/claude.md(Custom Endpoints and Gateways). -
conductor resume --webno longer shows a running workflow as stopped — a workflow that was paused from the dashboard (Stop, then Kill) recorded anagent_pausedevent in its event log with noagent_resumedcounterpart. On resume the CLI seeds the dashboard from that log, so the pause replayed and latched the dashboard's global paused state for the entire resumed run: the header showed Resume/Kill instead of Stop for a pause that never happened, hiding the only graceful stop behind a Kill that would hard-stop the healthy resumed run. Pause, iteration-limit-gate, and dialog events are now dropped on replay at every workflow depth, alongside the root lifecycle events already filtered; a gate the resumed run genuinely re-enters emits its own fresh event. Prior agent output and messages are still replayed. The dashboard's live-control buttons are also hidden inconductor replaymode, where the recorded-log server serves no/api/stop,/api/resume, or/api/killendpoint. -
Dashboard Stop/Resume/Kill no longer hang on a failed request —
fetchresolves rather than rejects on a 4xx/5xx response, so a non-2xx reply left the button disabled and reading "Stopping…" indefinitely, with nothing logged and no way back except reloading the page. The response status is now checked explicitly and surfaced next to the controls. -
Dashboard: expanding a subworkflow no longer slides the graph out from under you (#375) — the graph layout normalizes its bounding box to the origin on every rebuild, so growing one container repositioned every node while the camera stayed where it was. Expanding or collapsing a subworkflow now pans the viewport by the same amount the toggled container moved, so that container stays pinned under the cursor and the surrounding nodes visibly move out of its way instead. Collapsing that same subworkflow returns you to the view you started from. Expand-all, and any other change that toggles several subworkflows at once, anchors on whatever sits nearest the center of the pane; the view is not refit in either case. The same compensation steadies the graph when a running workflow's topology grows — a
for_eachfanning out or a subworkflow's DAG arriving.
0.1.27 - 2026-08-04
-
Skills:
runtime.skillsand per-agentskills:give agents opt-in knowledge bases (#180) —runtime.skillsenables a list of skills for every provider-backed agent in the workflow, and any agent can override it with its ownskills:(omitted = inherit,[]= explicit opt-out, a list = explicit set). Conductor ships a built-inconductorskill covering its own YAML schema, execution model, authoring patterns, and CLI commands, so an agent that writes or reviews workflows can be made Conductor-aware without pasting documentation into its prompt. The observable contract is the same on every provider — the agent has access to the named skill — but the mechanism differs:copilotregisters the skill directory on the SDK session, so only the frontmatter is read up front and the body is loaded on demand, while providers with no native skill surface receive the skill content injected into the prompt. Providers declare whether they can load skills at all, andconductor validaterejects a workflow that enables skills on one that cannot, rather than letting the content be silently dropped at run time. Skills are rejected on step types with no model behind them (script,set,wait,terminate,workflow,human_gate). Seeexamples/skills-self-improving-workflow.yamlanddocs/workflow-syntax.md(Skills). -
Skill discovery:
runtime.skill_discoverypicks up skills already installed on the machine (issue #362) —sources: [personal, project, plugins]scans~/.copilot/skillsand~/.claude/skills,.github/skillsand.claude/skillsfrom the workflow file's directory up to the repository root, and every installed plugin'sskills/directory, so a workflow can use a personal or team skill library without enumerating it. Off by default.exclude:drops individual skills by name. Conductor scans the union of both CLIs' locations itself rather than enabling each provider's own discovery: locations are provider-specific, so a per-provider flag would give acopilotagent and aclaude-agent-sdkagent different skill sets inside a single run. Scanning centrally also keeps Copilot'senable_config_discoveryoff, which would otherwise auto-load MCP servers from any.mcp.jsonin the working directory. Discovered skills joinruntime.skills, so an agent declaring its ownskills:(includingskills: []) still overrides them; a skill named inskills:beats a discovered one of the same name. Discovered content is held to a laxer standard than declared content — broken frontmatter, a taken name, an unreadable directory, or a skillclaude-agent-sdkcannot load are errors for a declared skill and warning-plus-skip for a discovered one (a provider with no native skill surface at all is the exception, and errors either way). Rejected at validation time onclaudeandhermes, which inject every skill body into every prompt and cannot bound a machine-dependent set; onclaude-agent-sdkonly discovered skills inside a Claude Code plugin are loaded and the rest are skipped with a warning.conductor validatelists what was found, where each skill came from, and the total size if eagerly injected. Seeexamples/skills-discovery.yamlanddocs/workflow-syntax.md(Discovering installed skills). -
skills:now accepts filesystem paths, not just built-in names (issue #350) — an entry is treated as a path when it starts with.or~, or contains/or\; everything else must still be a registered built-in, so a bareconductorcan never be shadowed by a same-named local directory. A path may point at a single skill directory (one holdingSKILL.md) or at a root of them, which expands to every immediate child that holds one. Relative paths resolve against the workflow file's directory — the same ruleworking_diruses — so a team can version a skill alongside the workflow that uses it with no per-developer install step, and the workflow resolves identically from any working directory. Conductor expands roots itself rather than handing them to a provider, because eager injection needs a name per skill andclaude-agent-sdkneeds a<plugin>:<skill>name; doing it centrally keeps every provider seeing the same set. Skill paths are trusted input by design: the same workflow file can already declaretype: scriptsteps running arbitrary shell, so no additional allowlist applies. -
runtime.skill_injectionbounds eagerly injected skill content (issue #350) —warn_bytes(default 64KB) logs a warning and reports fromconductor validate;max_bytes(default 128KB) fails the agent. Either can be set tonullto disable it. Providers without a native skill surface (claude,hermes) have no progressive disclosure:AgentExecutorprepends each enabled skill'sSKILL.mdplus its entirereferences/tree on every call and every retry, and there was previously no ceiling at all. The bundledconductorskill alone is ~117KB (~29K tokens), so the defaults deliberately straddle it — enabling it onclaudenow warns instead of breaking, while accumulating several large skills errors. Both limits are measured against the exact string being prepended and report a per-skill breakdown naming the offender. Providers with progressive disclosure (copilot,claude-agent-sdk) are unaffected. -
hermesdeclaresskills=True(issue #350) — the provider omittedskillsfrom itsCAPABILITIES, which defaults toFalse, soconductor validaterejectedskills:on it while its ownexecute()docstring described eager injection working. Injection happens inAgentExecutor, upstream of every provider, so the path was always reachable and the declaration was simply inaccurate. Now bounded byruntime.skill_injectionlikeclaude. -
claude-agent-sdkprovider now honorsworking_dir— the directory resolved fromagent.working_dir/runtime.working_diris forwarded toClaudeAgentOptions.cwd, so theclaudeCLI runs there and every stdio MCP server it spawns inherits the same directory. Previously the provider declaredworking_dir=Falseandconductor validaterejected any workflow that set it, which was accurate but left the provider out of step withcopilotandclaude. There is no per-server stamping as there is for Copilot, because the SDK's stdio server config has no working-directory field — inheritance from the CLI subprocess covers it. A missing directory still fails before the provider is reached, andstrict_mcp_configremains enabled so a.mcp.jsonsitting in the new directory cannot inject undeclared servers. TheclaudeCLI would also readCLAUDE.mdand.claude/settings*.jsonfrom its working directory, but the same release pinssetting_sourcesto an empty list (see the skills entry below), so those are no longer loaded from wherever the agent happens to run. Launch failures caused by a bad working directory are now reported as such rather than as connection problems, and are no longer treated as retryable. Seedocs/workflow-syntax.mdanddocs/providers/experimental.md. (#348) -
claude-agent-sdkprovider now supports MCP servers — workflow-levelruntime.mcp_serversare translated to the SDK's ownstdio/http/sseconfig shapes and passed throughClaudeAgentOptions, so an agent can use custom MCP tool servers and the built-inclaude_codetool preset at the same time. Previously the two were mutually exclusive: the provider declaredmcp_tools=Falseand the factory rejected any workflow declaring MCP servers. The generated config is written to a0600temp file and passed by path so resolvedenvvalues andAuthorizationheaders never reach theclaudeCLI's command line, andstrict_mcp_configis always enabled so ambient project/user MCP config cannot inject undeclared servers. A narrowing per-servertools:filter has no SDK equivalent and is refused (when the first agent on this provider runs) rather than silently ignored. Seeexamples/claude-agent-sdk-mcp.yamlanddocs/mcp-tools.md. (#335) -
Conservative unwrapping of wrapper-shaped scalars. When a scalar output field receives an object holding exactly one value of the expected type under either the field's own name or a generic
value/resultkey, providers unwrap it and log a warning instead of spending a recovery round-trip. Ambiguous shapes (two matching candidates) and any other key shape are left alone and re-prompted, so an object like{"error": "I could not complete the task"}is never laundered into an answer. Validation itself stays strict, sosetandscriptstep output is unaffected. (#343) -
Non-object JSON responses are recoverable. A response parsing to a bare scalar,
null, or an array is now re-prompted as a shape failure rather than producing an unhelpful terminal error. On Claude this previously reachedvalidate_outputinside the API error handler and surfaced as "check API key, model name, and request parameters"; on Copilot it reached the executor backstop and was reported as a missing required field. (#343) -
agent_parse_recoveryevent. Recovery attempts were previously visible only under verbose console logging, so a run could burn its entire recovery budget without leaving a trace. All three providers now emit an event carrying the attempt number, the budget, whether the cause wasschemaorsyntax, and the error. Rendered in the dashboard activity stream, the console, and the structured event log. (#343) -
Output validation errors describe the offending value. Container values are rendered by shape (
object with keys ['a', 'b']) rather than dumped, sincevalidate_outputalso runs onsetandscriptstep output that may carry secrets. (#343) -
Sub-workflow nodes in the dashboard are expandable before they run. A
type: workflownode previously became expandable only once the engine actually reached that step, so the inner DAG of a sub-workflow that had not started yet was invisible. Conductor now resolves each sub-workflow's static topology up front and attaches it to the graph, so the real inner DAG can be expanded — recursively, for nested sub-workflows — from the moment the run starts. Resolution is best-effort: a missing file, registry error, cycle, or depth limit simply leaves the node collapsed until the engine reaches it. (#360)
-
A malformed
SKILL.mdno longer fails silently (issue #350) — both the Copilot CLI and Claude Code skip a skill whose YAML frontmatter cannot be parsed, with no warning and no error, leaving an agent running without the knowledge its author asked for. The trap is ordinary: adescriptioncontainingTriggers: ...as an unquoted plain scalar is invalid YAML. Conductor now parses the frontmatter itself, requires a non-emptynameanddescription, and reports the underlying YAML error along with thedescription: |block-scalar fix. Enforced during resolution rather than only inconductor validate, becauseconductor runnever invokes the static validator. -
conductor validaterejects aclaude-agent-sdkskill outside a plugin (issue #350) — that SDK exposes no bare skill-directory option, only plugin roots plus skill names, so such a skill is unreachable there even thoughcopilotloads it fine. It previously surfaced as a runtimeProviderErroron first execution; it is now reported before the run starts, naming the directory and offering both remedies (package it as a plugin, or run the agent oncopilot). -
skills: []is now a real opt-out onclaude-agent-sdk, and agents no longer inherit ambient skills from the machine. The provider left the SDK'ssetting_sourcesunset, so theclaudeCLI discovered and enabled skills from~/.claude/skills/, every.claude/skills/up the directory tree, and enabled plugins — none of which the workflow declared, and all of which varied by developer machine and launch directory. Conductor documentsskills: []as an explicit opt-out; on this provider it silently opted out of nothing. Two options now carry that fix together and neither is redundant:setting_sourcesis always[](the same unconditional isolationstrict_mcp_configalready applies to MCP servers), andskillsis always passed explicitly, because the SDK treats an omitted list as "CLI defaults apply" and re-defaultssetting_sourcesto["user", "project"]wheneverskillsis set without it. Behavior change: agents on this provider also stop picking up ambientCLAUDE.md,.claude/rules/*.md, user/project/localsettings.json(includingenvandapiKeyHelper), and hooks. Instruction files can be supplied explicitly with--workspace-instructions(or--instructions); settings and hooks have no equivalent, so move anything load-bearing there into the environment. Note the SDK's skill list is a context filter, not a sandbox — undeclared skills are hidden from the model's listing, but their files stay readable on disk. (#352) -
tools: []no longer fails validation when no MCP servers are declared — the capability cross-check rejected an explicit empty allowlist against any provider withmcp_tools=Trueandworkflow_tools_passthrough=False(such asaca), even when the workflow declared nomcp_serversand therefore had nothing to forward. The check is now gated on MCP servers actually being configured. (#335) -
Schema-shape failures now go through the parse-recovery loop instead of killing the workflow. When an agent returned syntactically valid JSON whose fields had the wrong shape (for example an object where
type: stringwas declared), the Copilot and Claude providers failed the run immediately with zero recovery attempts, even thoughmax_parse_recovery_attemptsexists for exactly this class of contract violation. Schema validation ran one layer up inexecutor/agent.py, after the provider had already returned and — for Copilot — after its SDK session had been disconnected, so the loop that could have re-prompted never saw the error. Both providers now validate inside the recovery loop, matching Hermes. (#343) -
Exhausted recovery keeps the specific validation error. A schema-shape failure that survives every recovery attempt now re-raises the original
ValidationErrornaming the offending field and its expected type, rather than collapsing into a generic "failed to parse structured output" provider error. Syntax failures keep raisingProviderErroras before. This also fixes Hermes, which previously discarded the field detail. (#343) -
Hermes now honors
retry.max_parse_recovery_attempts. It used a hardcoded module constant of 3 and silently ignored the YAML value that Copilot and Claude both respect. (#343) -
Nested
arrayitem schemas inoutput:are now enforced.validate_outputtype-checked only the top level of an array, so anarray<object>output whose items were missing declared fields — or had the wrong types — passed validation silently, even though the full nested schema had been sent to the model. Validation now recurses through bothobject.propertiesandarray.itemsat every depth, for LLM agents,setsteps, andscriptsteps alike. Behavior change: a workflow that was quietly emitting output violating its own declared nested schema will now fail withValidationErrorinstead of passing. Flat schemas andobjectnesting are unaffected, and arrays declared withoutitemskeep their existing passthrough. (#337) -
MCP server connections are now closed by the task that opened them. Each stdio/session lifecycle is held in its own owner task and signalled at shutdown, so AnyIO cancel scopes always exit in the task that entered them. Previously a connection opened in a worker task and closed from the root task could raise a cancel-scope error during teardown, surfacing as a spurious failure at the end of an otherwise successful run. Cleanup failures are logged rather than masking the caller's own cancellation, and registering a duplicate server name is now rejected instead of orphaning the existing connection. (#353)
-
Non-ASCII agent output is no longer truncated ~6x too aggressively before semantic validation. The
validator:grader and the dialog-trigger evaluator serialized the agent output with ASCII escaping before applying their fixed character budget, so every Cyrillic or CJK code point cost 6 characters and every emoji 12. Non-English output reached the grader with a fraction of the source material an equivalent English output would get, and the cut could land mid-escape, leaving malformed JSON in the prompt. Both now serialize without escaping, so the budget is measured in real characters for every language. (#356) -
An inline-expanded sub-workflow now follows the live run after a loop-back. When a loop-back route re-invoked the same sequential sub-workflow, the inline graph expansion stayed pinned to the first, already-completed invocation, even though double-click navigation and the Activity tab correctly tracked the live one. Inline expansion now resolves newest-first, matching the rest of the dashboard. (#361)
-
Every historical sub-workflow iteration is now reachable from the dashboard. In the "Subworkflow Runs (N)" list, each row resolved by slot rather than by position, and every re-invocation of a sequential sub-workflow shares a slot — so clicking an older, completed run always landed on the most recent one. Rows now navigate to the exact run clicked, are labelled
Iteration Nonce a slot repeats, and the breadcrumb trail says which iteration is being viewed. Following the live run (double-click, inline expansion, deep links) is deliberately unchanged. (#365) -
Handled fail-open paths no longer print a full traceback at WARNING level. When a semantic validator rejected an output and the retry itself failed, Conductor correctly kept the original output and carried on — but logged the warning with a complete Python traceback, making a recovered run look like a crash. The warning is now a single line naming the exception type and message, with the traceback kept at DEBUG. The same treatment was applied to the sibling best-effort paths: validator call failures and timeouts, provider session-ID collection for checkpoints, checkpoint save and rotation failures, and event-callback errors in
hermes. (#357)
-
The
claudeprovider now runs its agentic loop through Pydantic AI. The hand-written inner loop was replaced with a Pydantic AI runtime while keeping Conductor's provider contract at the boundary — the sameAgentOutput, event vocabulary, retry and interrupt semantics, usage accounting, MCP policy, and output validation. The visible gain is streaming:claudenow emits model, reasoning, and tool events as they happen rather than only at completion, so the dashboard, console, and event log follow a Claude agent live the way they already followed Copilot. Structured output is produced natively by the runtime and still re-validated against the declaredoutput:schema. This addspydantic-ai>=1.44.0as a required runtime dependency, so aconductorupgrade pulls in a larger dependency set than before. (#355) -
claude-agent-sdknow loads skills natively instead of injecting them into every prompt. The provider previously took the eager preamble path on the grounds that the SDK had no skill surface — out of date, and expensive: the fullSKILL.mdplus the entirereferences/tree was prepended to every call and every retry (~29K tokens for the bundledconductorskill). The owning Claude Code plugin is now registered on the session and the skill enabled by its<plugin>:<skill>name, so the CLI reads only the frontmatter up front and loads the body on demand. An agent with an explicittools: []is granted back the singleSkilltool when it has skills enabled, since an empty base tool set would otherwise leave the declared skill unreachable. Wheels now also shipplugins/conductor/.claude-plugin/; without the manifest no plugin root resolves at all, so a non-editable install would fail every skills-enabled agent on this provider. (#352) -
The
claude-agent-sdkoptional dependency floor is nowclaude-agent-sdk>=0.2.82— the 0.2.x line is what Conductor tests against. (#335) -
claude-agent-sdkagents no longer inherit ambient MCP configuration. Conductor now always setsstrict_mcp_config, so a project.mcp.json, user-global settings, or plugin-provided servers are ignored and only servers declared inruntime.mcp_serversattach. Workflows that relied on Claude Code's own MCP settings must declare those servers in the workflow. (#335)
0.1.26 - 2026-07-27
- New experimental
acaprovider (Azure Container Apps) — delegates an agent's entire agentic loop, tools, and MCP calls to a remote ACA dynamic-sessions sandbox instead of running it on the host, so untrusted or isolation-sensitive agents execute in a disposable container. Includes provisioning tooling (scripts/aca/provision-pool.sh), an in-package runner image, an end-to-end example (examples/aca-coding-agent.yaml), and automatic inner-Copilot credential resolution — falling back throughCOPILOT_PROVIDER_BASE_URL(BYOK) →COPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN→gh auth token— so an operator already signed in with the GitHub CLI needs no ACA-specific credential setup. Seedocs/providers/aca.mdfor architecture, setup, and the declared experimental-tier capability carve-outs. (#284)
- Copilot and Hermes prompt schemas no longer fall back to a synthetic
"The {field} field"description, matching Claude's existing behavior — fields without an explicitdescriptionnow produce a shorter JSON Schema object instead of a made-up one. Hermes also now shares the same recursive prompt-schema builder as Copilot, fixing cases its old hand-rolled builder got wrong:array<object>items now includerequiredalongsideproperties, andarray<array>nesting recurses correctly instead of collapsing. (#317)
0.1.25 - 2026-07-21
- Web dashboard warns when stuck reconnecting after a silent crash — a
new amber banner appears if the dashboard's WebSocket client has been
disconnected and retrying for more than 60 seconds while a workflow is
still marked "running", pointing at the best available log
(
--web-bg's captured stderr/stdout log, the--log-filedebug log, or the launching terminal) so a silently crashed--web-bgprocess is no longer indistinguishable from a healthy, still-running workflow. (#332)
0.1.24 - 2026-07-21
- Grouped CLI command surface — related subcommands are now organised under
noun groups:
conductor checkpoint list(wasconductor checkpoints) andconductor gate respond(wasconductor gate-respond), alongside the existingregistrygroup. The root--helpgroups commands into Run & Recover, Author & Inspect, Environment, Interact, and State panels, while the hot-path verbs (run,resume,validate,show,stop,replay,update,doctor) stay flat. (#275)
conductor checkpointsandconductor gate-respond— replaced byconductor checkpoint listandconductor gate respondrespectively. The old names still work and forward to the new commands, but print a one-line stderr deprecation warning, are hidden from--help, and will be removed in a future release. (#275)
--web-bgno longer aborts when a workflow contains ahuman_gate— the dashboard already supported resolving gates (modal, WebSocketgate_response,POST /api/gate-respond,conductor gate-respond), but the detached background process raced a CLI prompt against it and crashed withEOFErroron its closed stdin. The gate now waits web-only when running in--web-bg(or any non-TTY context), and the CLI prints a notice pointing at the dashboard URL andconductor gate-respondinstead of aborting the launch. (#286)--web-bgcould hang forever if no dashboard client ever connected — the auto-shutdown grace timer was only armed from WebSocket-disconnect code paths, so an unwatched run that finished before anyone opened the dashboard never started its grace countdown and the detached process became a zombie holding its port and PID file. The timer now also arms on the workflow's root completion event. (#318)conductor doctorshowed thecopilotprovider as red/unconfigured even when fully authenticated — credential env vars are now modeled as optional per provider; an absent optional credential (e.g.copilot's GitHub/Copilot CLI login) renders as a neutral○with an explanatory note instead of a red✗, while genuinely required credentials (e.g.claude'sANTHROPIC_API_KEY) still flag as missing. (#319)- Web dashboard could keep showing a stale UI after
conductor update—index.htmlis now served withCache-Control: no-cacheso the browser revalidates it on every load and always picks up the current build's version-hashed asset bundle. CI also now fails the Frontend job if the committedstatic/bundle doesn't match a freshmake build-frontend, so a frontend change can no longer merge without its built assets. (#321)
0.1.23 - 2026-07-20
working_dirfor LLM agents and their MCP servers — agents gain an optionalworking_dir(with a workflow-wideruntime.working_dirdefault) so an agent and its MCP servers run in a chosen directory. It is resolved with precedence agent > runtime >os.getcwd()and accepts static values or Jinja templates. The Copilot and Claude providers stamp the resolved directory onto the agent session and each MCP server;conductor validaterejectsworking_diron providers that don't support it (hermes,claude-agent-sdk) and onwait/set/terminate/human_gate/workflowstep types. See the "Working Directory" section ofdocs/mcp-tools.md. (#297)runtime.tool_outputlimits for oversized MCP tool results — a configurable per-result cap stops a single large MCP tool result from overflowing the model's context window with a fatal token-limit error. Oversized results are truncated (max_chars, default50000) and the full text is spilled to a temp file the agent can page through, with a notice surfaced in the console, event log, and dashboard. Claude truncates conductor-side; Copilot forwards the limit to its native SDK spill feature; the setting is ignored byclaude-agent-sdk(managed via the nativeMAX_MCP_OUTPUT_TOKENS) and is N/A forhermes. Seeexamples/tool-output-limits.yamland the "Tool output limits" section ofdocs/mcp-tools.md. (#313)- Inline expand/collapse for subworkflows in the dashboard graph —
subworkflow nodes now expand inline (collapsed by default) to reveal their
internal DAG without leaving the current view, alongside the existing
double-click drill-down "focus mode." Adds an Expand/Collapse-all toolbar
control and an
Ekeyboard shortcut, and expandsfor_each-of-workflow groups into inline sub-containers. (#316)
- Subworkflow-adjacent agent node could appear stuck "running" in the
dashboard — the graph store now clones the
subworkflowContextstree on every event, so selectors keyed on it reliably observe nested status updates. An agent node next to atype: workflowstep no longer renders as "running" after its underlying data is alreadycompleted(a pure rendering desync, not an engine data bug). (#308)
0.1.22 - 2026-07-15
-
conductor doctor --modelssurfaces per-model reasoning-effort support and context-window limits — a new optionalAgentProvider.get_model_capabilitieshook (alongside the existingget_max_prompt_tokens/get_model_pricinghooks) reports, per model: whichreasoning.effortlevels it accepts, its default effort, and its prompt/output/context-window token limits.--modelsnow renders a separate per-provider Models detail table with this data (the Providers table's Models column shows a count); the JSONmodelsfield is now a list of capability objects rather than plain id strings. The Copilot provider implements the hook fully viaclient.list_models(); the Claude provider derives reasoning-effort support from the existing thinking-model heuristic and reports prompt tokens only (the Anthropic SDK exposes no output/total-context split); other providers (claude-agent-sdk,hermes,openai-agents) don't implement model enumeration at all, so they shown/ain the Providers table and get no Models detail table. See the "Per-model capabilities" section indocs/cli-reference.md. (#301) -
maxreasoning-effort level — the unified reasoning scale is nowlow | medium | high | xhigh | max, unifying it with the GitHub Copilot CLI. On the Copilot providermaxis forwarded to the SDK and still validated per-model against the model's advertisedsupported_reasoning_efforts. On the Claude providermaxmaps to a59904-token extended-thinking budget (64000 − 4096, the largest budget that keeps the default answer headroom under the 64000-token output cap); both the main agentic-loop and dialog-turn code paths share the same clamping helper, so the cap is enforced consistently. The experimental Hermes provider keeps the original four levels —maxis rejected both statically (conductor validate) and at execute time (including when a Jinja-templatedreasoning.effortonly resolves tomaxafter rendering). (#299)
-
Copilot per-model
reasoning_effortvalidation was a silent no-op —_validate_reasoning_effort_for_modelreadcapabilities.supported_reasoning_efforts, but the installedgithub-copilot-sdk(>=1.0.0) exposes that field (anddefault_reasoning_effort) at the top level of theModelobject, not nested undercapabilities. The lookup always returnedNone, so the per-model check (including themax-rejection behavior from #299) never fired against the real SDK — a model withoutmaxsupport would only be caught by the backend, not by Conductor's own validation. Fixed to read the correct field; discovered and corrected while implementing #301, which needed the same field fordoctor --models. -
Install-script tests no longer pollute the developer's shell profile — the install scripts ran
uv tool update-shellunconditionally, so the-m install_scriptsintegration tests appended each run's throwawayUV_TOOL_BIN_DIRto the real~/.zshenv(and shell equivalents). Those stale entries shadowed the user's actualconductorinstall with an oldv0.0.2test fixture, soconductorreported the wrong version andconductor updateappeared to do nothing.install.sh/install.ps1now honorCONDUCTOR_INSTALL_SKIP_PATH_UPDATE=1(and a matching--skip-path-update/-SkipPathUpdateflag) — set by default in the test harness — and a regression test asserts the scripts never touch shell profiles.
- README provider comparison table corrected — set Context Window to "Per-model" across all providers, set Pricing to "Subscription" for Copilot and Claude Agent SDK, labeled Claude Agent SDK as experimental in the top-level Features list (matching the Providers table's Tier column), and added a "Using Copilot" section with a config example and auth notes. (#296)
0.1.21 - 2026-07-13
- Connect the Copilot provider to an existing runtime —
runtime.providergains a Copilot-onlyruntime_url(plus optionalruntime_token) that points Conductor at an already-runningcopilot --headlessprocess instead of spawning its own nested one. Agents share the authenticated runtime process while retaining separate SDK sessions. Both fields also resolve from the namespacedCOPILOT_PROVIDER_RUNTIME_URL/COPILOT_PROVIDER_RUNTIME_TOKENenvironment variables, which activate the connection with no YAML — the zero-config path for external orchestrators that already own an authenticated Copilot process. Runtime transport can be combined with custom model-provider routing. Seeexamples/copilot-existing-runtime.yamland the "Connecting to an Existing Copilot Runtime" section ofdocs/configuration.md. - Provider-supplied model pricing — cost reporting now resolves pricing via a
new
AgentProvider.get_model_pricinghook before falling back to the static table. Resolution order is workflowcost.pricing→ provider hook → built-inDEFAULT_PRICING→ unpriced. The Copilot provider derives live rates from its SDK billing metadata (AI Credits → USD), so newly-released models are priced without waiting for a table refresh; providers whose SDK exposes no pricing (e.g. the Anthropic API) fall back to the table. (#265) conductor doctor— provider & environment diagnostics — a safe, read-only command that reports which providers are installed, their capability tier (stable/experimental), which credential environment variables are detected (presence only — values are never printed), plus Conductor version / update status and configured registries. Offline by default;--checktests provider connections,--modelslists available models,--provider NAMEscopes to one provider, and--jsonemits machine-readable output for CI. Exit code is1only when--checkis set and the scoped provider (defaultcopilot) fails to connect. Also adds a publiclist_models()method to the provider interface (implemented for Copilot and Claude). (#274)- Jinja
include/import/extendsin!file-loaded prompts — prompt templates loaded viaprompt: !file ...(andsystem_prompt: !file ...) now support loader-dependent Jinja constructs, resolved relative to the prompt file's own directory, enabling reusable prompt partials across workflows. Inline prompts that attempt these constructs get a clear error instead of a confusing failure. (#291, closes #287)
- Cost summary silently undercounted unpriced models — the run summary summed
only the priced subset of agents and presented it as the complete total, so
spend on models without available pricing vanished with no signal. Unpriced
agents are now surfaced in the CLI summary and the web dashboard status bar
(
~$X (N agents unpriced: model-a, model-b)), so a partial total is never shown as a clean, complete number. (#265) - Missing pricing for current models costed them at $0 — several current
models had no
DEFAULT_PRICINGentry, soget_pricingreturnedNoneand they were silently costed at $0 in the Token Usage Summary / cost breakdown (dotted version suffixes likeclaude-opus-4.8are not bridged by the--delimited fuzzy fallback). Added entries forclaude-opus-4.7,claude-opus-4.8,claude-sonnet-5,gpt-5.3-codex,gpt-5.4,gpt-5.5,gpt-5-mini,gpt-5.4-mini, andgemini-3.5-flash. (The related sub-workflow usage under-reporting in the same issue was already fixed in #212.) (#266) for_eachinline agents skipped most provider-capability checks —conductor validate's provider-capability cross-check applied the full per-agent matrix (reasoning effort, structured output, per-agent MCP provider override, explicitmax_session_seconds) only to top-levelagents:. Afor_eachgroup's inline agent — which runs at runtime exactly like a top-level agent — was checked for tool allowlists only, so it could request a capability its provider doesn't support (e.g.reasoning.effort: highon theclaude-agent-sdkprovider), pass validation, then fail or silently degrade mid-iteration. The per-agent checks are now shared and run over inline agents too, and the workflow-levelmcp_servers/max_session_secondsinheritance checks now also account for inline agents on the default provider. (#270)- For-each dive-in worked only for finished items — in the web dashboard's
for-each group detail panel, the per-item "Dive into subworkflow" control was
nested inside the row's expand/collapse
<button>, which isdisabledwhile an item has no expandable details (a running workflow-type iteration that has not yet produced a prompt/output/activity/error). A disabled button suppresses clicks across its whole subtree, so dive-in only fired once the item had failed or completed. The toggle and the dive-in control are now siblings, so dive-in stays clickable for running items too. (#273) - Custom
defaultJinja filter rejected the standardbooleanargument — Conductor's override of Jinja2's built-indefaultfilter only accepted two parameters, so valid templates using the standard thirdbooleanargument raised aTypeError. The filter now matches Jinja's built-in signature (default(value, default_value="", boolean=False)) while preserving Conductor's existing two-argument behavior whenbooleanis omitted. (#292, closes #288) - Claude provider silently dropped
agent.system_prompt— the nativeclaudeprovider (Anthropic Messages API) ignoredsystem_promptinstead of sending it. It is now passed as the native top-levelsystemparameter on every API call in the agent execution path (main loop, tool-use iterations, parse recovery, interrupt partial output, retries), and the validator rubric's system prompt now reaches the model the same way. (#293, closes #289)
--quiet/--silentdocumented asrunoptions — both flags are root-level options defined on the app callback, so they must appear before the subcommand (conductor --quiet run workflow.yaml); placing them after it is rejected with "No such option".docs/cli-reference.mdand the README listed them in theconductor runoptions table, whiledocs/cli-reference.mdalso showed post-subcommand examples (conductor run workflow.yaml --quiet) that fail as written — as did theconductor run --helpepilog. Moved the flags to a new Root-Level Options section, corrected the examples (docs and--helptext), and fixed the same mis-ordering in the conductor skill's quick reference.
0.1.20 - 2026-06-26
- Hermes provider (experimental) — optional third provider built on the
NousResearch
hermes-agentlibrary, which manages its own tool ecosystem (no MCP configuration). Install separately withpip install hermes-agent; Conductor works without it. Declared in the experimental provider tier with documented capability carve-outs (no MCP servers, no per-agent workflowtools:allowlist, structured output via prompt injection). Supports custom endpoints via structuredruntime.provider(base_url/api_key),hermes_homeprofiles,hermes_toolsets, streaming + reasoning event callbacks, cooperative interrupt, session-history resume, andmax_session_seconds. Seedocs/providers/hermes.md. (#235) - Cost budget enforcement — set a USD ceiling for a run with
runtime.budget_usdand choose how the engine reacts viaruntime.budget_mode:audit(default — track spend and warn as the cap is approached) orenforce(stop the run once the projected cost would exceed the cap). Off by default — no budget tracking happens unlessbudget_usdis set. (#212) - External-workflow friction improvements — four knobs that surfaced while
running real-world workflows: per-agent
output_mode(raw|envelope) for cross-provider output-shape parity;retry.max_parse_recovery_attemptsto cap the in-session JSON-correction prompts sent when an agent's output fails to parse; a newconductor gate-respond --port <port> --choice <value>command to resolve a human gate from the CLI (handy for--web/--web-bgruns); and more robust Windows path handling. (#234) - Templated
reasoning.effortandcontext_tier— both per-agent fields now accept Jinja2 templates (e.g.effort: "{{ workflow.input.eff }}") resolved at runtime against the workflow context, instead of being rejected at YAML load as invalid enum literals. The rendered value is re-validated against the allowed set. (#263, closes #262) - Scoped
applyToinstruction loading —.github/instructions/*.mdfiles with a scopedapplyToglob (e.g.**/*.cs,services/foo/**) are now loaded when their scope overlaps the run's working directory, instead of being silently dropped unlessapplyTo: "**". Multi-glob values separated by;or,are supported, and the closest-owning convention directory wins for nested instruction files. (#238, closes #231)
- Copilot model attribution for auto-routed runs — when an agent uses
model: auto,AgentOutput.modelnow records the concrete model the Copilot SDK resolved to (captured from theassistant.usageevent) instead of the literal string"auto", so token usage and cost are attributed to the real model. (#268) - claude-agent-sdk default tool preset — an agent that omits
tools:on theclaude-agent-sdkprovider again receives the fullclaude_codepreset instead of zero tools. The omitted-vs-empty distinction (tools:absent means "all tools";tools: []means "none") was being lost at the executor→provider boundary whenever the workflow declared no MCP tools. (#269)
0.1.19 - 2026-06-16
- Context tier — new
context_tierknob (default|long_context) to select a model's long-context (e.g. 1M-token) window on the Copilot provider. Set per agent viacontext_tier:(sibling tomodel) or workflow-wide viaruntime.default_context_tier; the per-agent value wins. It composes independently withreasoning.effort(the two map to separatecreate_sessionkwargs). The Copilot provider forwards the resolved value ascontext_tiertocreate_session; other providers ignore it. Only valid on standardagent-type agents (rejected onscript,human_gate, andworkflowagents). Seeexamples/context-tier.yamland Context Tier. (#251) - Validator block — an optional
validator:block on provider-backed agents that runs a second LLM call to grade the agent's output against a user-defined rubric. Distinct fromretry:(transient failures, same prompt) andoutput:(shape/type): it catches structurally valid but semantically wrong, incomplete, or off-rubric output. Fields:criteria(required),model(defaults to the agent's model), andmax_retries(0or1, default1, hard-capped at1). Onpassed: falsethe agent re-runs once with the validator's issues appended under a## Validation feedbacksection; the second output is final (no second validation loop). Validation is fail-open — grader errors or unparseable responses are logged and treated as a pass, so a hung or broken grader can't block the workflow. Wired into the main loop, parallel groups, and for-each loops; emitsagent_validator_start/agent_validator_complete/agent_validation_failedevents surfaced in--verboseconsole output and the web dashboard, and records the grading call (plus any discarded first attempt) as a separate<agent> (validator)usage row. Rejected onscript/human_gate/workflow/wait/set/terminatesteps. Seeexamples/validator.yamland the Validator section of the workflow syntax docs. (#256, closes #220) - Periodic / milestone checkpoints — opt-in
runtime.checkpointblock that saves a resumable checkpoint at step boundaries so a long run that stalls or is hard-killed stays recoverable (previously Conductor only checkpointed on failure). Two OR-combined triggers —every_agent: true(save at every step boundary) andevery_seconds: N(a throttle: save at the first boundary onceNseconds have elapsed since the last checkpoint) — pluskeep_last(default5) to rotate older periodic checkpoints per run. Off by default, so failure-only behaviour is preserved. Periodic checkpoints are scoped to their own run and trigger, so failure checkpoints and other runs' files are never rotated away, and they are cleaned up automatically on clean completion or an explicitterminate. A failed periodic save emits acheckpoint_save_failedevent (console + JSONL + dashboard) so a recovery-reliant run is never left silently without checkpoints, andconductor checkpointsgains aTriggercolumn. Seeexamples/periodic-checkpoints.yamland the Periodic Checkpoints section of the workflow syntax docs. (#255, closes #244) - Script
stdin:payload transport —type: scriptsteps accept a newstdin:field: a Jinja2 string template rendered against the workflow context and piped to the child process on stdin as UTF-8. Workflows can now hand large or structured payloads to scripts without hitting command-line length limits (notably Windows, which caps the command line at ~32 KB), removing argument-name-specific temp-file workarounds.stdinandargsare orthogonal; an explicit empty string pipes an immediate EOF, and omitting it preserves the legacy behaviour of inheriting the parent's stdin. The payload is streamed in the background so multi-MB inputs can't deadlock, the submitted byte count is surfaced asstdin_byteson thescript_completedevent, and an unencodable payload raises a namedExecutionError. Rejected on every non-script step type. Seeexamples/script-stdin.yaml. (#253, refs #18) - Experimental provider tier — providers that delegate part of the agentic
loop to an upstream SDK can now declare an experimental stability tier with
explicit, allowed capability carve-outs instead of silently eroding provider
parity. Every provider declares a class-level
CAPABILITIES(ProviderCapabilities) descriptor, andconductor validatecross-checks workflow features against each agent's resolved provider — surfacing silent mismatches (unsupportedruntime.mcp_servers, non-empty per-agenttools:allowlists,reasoning.effort, structuredoutput:, concurrent use in parallel/for-each groups,max_session_seconds) at validate time rather than at runtime. Theworkflow_startedevent gains aprovidersblock (tier, upstream pin, maintainer, full capability dump) plus aprovider_nameper agent; the CLI prints a one-time banner per experimental provider per run, and the web dashboard renders a yellow "exp" badge on affected agent nodes. See Experimental Providers. (#242, closes #241) claude-agent-sdkprovider — a new, experimental provider that delegates the agentic loop, tool execution, and structured-output extraction to the Claude Code CLI via theclaude-agent-sdkpackage. Unlike the rawclaudeprovider it does not manage its own retry logic, MCP servers, or tool wiring — the SDK runtime owns those. It achieves event and output parity (agent_turn_start,agent_message,agent_reasoning,agent_tool_start/agent_tool_complete, and the standardAgentOutputshape), pairing realToolResultBlockresults rather than emitting nulls. Workflow-levelruntime.mcp_servers, non-empty per-agenttools:lists, andtemperature/max_tokensare rejected at the factory (the CLI controls these). Install with the optional extra (pip install conductor[claude-agent-sdk]) plus theclaudeCLI. Seeexamples/experimental-claude-agent-sdk.yaml. (#104)
- Web dashboard Stop/Kill now always writes a checkpoint (or clearly
explains why it couldn't). Previously, killing a run while an agent was
actively executing — or clicking Stop during the brief startup window before
the engine bound its interrupt event — cancelled the engine task from the CLI
wrapper, bypassing the engine's failure handling so no checkpoint and no
workflow_failedevent were produced and progress was silently lost. Now:- A dashboard stop that cancels the engine routes through a best-effort
checkpoint +
workflow_failed/checkpoint_savedemit (WorkflowEngine.handle_dashboard_stop), so the run is resumable withconductor resume. POST /api/stopduring startup is queued until the interrupt event is bound (graceful pause path) instead of falling back to a hard cancel.- The dashboard shows a dedicated, calm "Workflow Stopped" banner with
Checkpoint saved: <path>— orNo checkpoint could be saved — <reason>when one genuinely couldn't be written — instead of an alarming red "Workflow Failed". (#245)
- A dashboard stop that cancels the engine routes through a best-effort
checkpoint +
human_gateagents: the dict returned byprompt_fortext-collection fields is no longer spread into the gate's output root, where it could silently overwrite the reservedselectedkey (e.g. an option declaringprompt_for: selectedwould clobber the chosen option value with whatever the user typed). Collected values are now nested under an explicitadditional_inputkey, matching the shape thegate_resolvedevent already used. (#237)context: explicitmode now supports nested output projection ininput:declarations. References of the formagent_name.output.field.subfield...(and theagent_name.field.subfield...shorthand) project arbitrarily deep into a prior step's structured output, where previously only a single level (agent_name.output.field) resolved and deeper paths silently failed. Optional refs (?) skip missing intermediate paths, and projected leaves are deep-copied to avoid mutation aliasing. Static validation inconductor validatewas aligned with the runtime so valid nested references no longer fail validation. (#239)
- BREAKING (templates) —
human_gateoutput shape changed.- Before:
{{ <gate>.output.<prompt_for_field> }}(root-level). - After:
{{ <gate>.output.additional_input.<prompt_for_field> }}(nested). - Gates without any
prompt_fornow produceadditional_input: {}rather than just{"selected": ...}— the key is always present. <gate>.output.selectedis unchanged.- Templates that referenced the old flat path now raise
TemplateError(StrictUndefined), so the migration fails loudly rather than rendering to empty strings. - In
context: explicitmode,input:declarations can reference either<gate>.output.additional_input(the whole dict) or an individual<gate>.output.additional_input.<field>. Nested explicit-input projection landed in this same release (#239), so the dotted field path now resolves directly; you can also still declare the parent and read individual fields via Jinja2 in the consuming agent's prompt or output template.
- Before:
0.1.18 - 2026-05-28
- New
type: setworkflow step that evaluates Jinja2 expressions and binds the results into the workflow context — no LLM call, no subprocess, no I/O. Two surface forms:value:(single expression bound as<step>.output, scalar / list / dict by auto-detection or explicitoutput_type:) andvalues:(named bindings rendered in one pass against the pre-step context and bound as<step>.output.<key>). Type detection defaults to YAML auto-parsing with a JSON-safety pass that convertsdatetime/date/timeto ISO 8601 strings and raisesExecutionErroron other non-JSON-safe values (including non-string dict keys) so checkpoint round-trips stay stable. Explicitoutput_type:(single-valueonly) supportsstring,number,integer,boolean,list,dict. The engine dispatches set steps in the main loop, parallel groups, and for-each groups via the shared_run_set_stephelper, emittingset_started/set_completed/set_failedand enforcing theoutput:schema (rejected for scalar outputs with a friendly suggestion).WorkflowContext.storewas widened to accept any JSON-safe value;_add_agent_inputreturns scalars verbatim forstep.outputand raises a clearKeyErrorforstep.output.fieldshorthand on non-dict outputs. The web dashboard adds a dedicatedSetNode(variable icon, key count / value preview) andSetDetailpanel showing output type, bindings, and rendered value. Newexamples/set-step.yamldemonstrates single + multi binding plus a boolean route on the derived flag (#226, closes #221). - New
type: waitworkflow step that pauses execution for a parsed duration via in-processasyncio.sleep. Cross-platform — no shellsleepdependency. Use for rate-limit cooldowns, polling intervals, external-system catch-up, and demos. Theduration:field accepts plain numbers (seconds), suffixed strings ("500ms","60s","2.5m","1h"), or a Jinja2 template that renders to one of those (e.g."{{ workflow.input.poll_interval }}s"). Schema enforces0 < duration <= 24hand rejects boolean values pre-coercion.Esc/Ctrl+Gcancels in-progress waits immediately (the engine races the sleep against the interrupt event), and the workflow-levellimits.timeout_secondsalso cancels them. Wait steps emitwait_started/wait_completed/wait_failedevents alongside the genericagent_started(withagent_type: "wait"), so existing dashboards keyed on agent lifecycle pick them up automatically. The dashboard adds a dedicatedWaitNode(clock icon) andWaitDetailpanel that show the requested duration, actual elapsed time, reason, and an "interrupted" indicator. The public output contract is strict — only{"waited_seconds": float}is exposed to workflow context; extra metadata lives in event payloads. Wait steps count towardlimits.max_iterations(each pause is one step) but are not subject tomax_agent_iterations(per-LLM-agent tool counter). Wait cannot be used insideparallelorfor_eachgroups. Newexamples/wait-step.yamldemonstrates a polling pattern with a templated poll interval and route loop-back (#224, closes #218). - New
type: terminateworkflow step that explicitly ends the workflow with a structuredstatus(success|failed) and Jinja2-renderedreason, plus an optionaloutput_template(dict[str, str]) that replaces the workflow-leveloutput:mapping for that termination path. Reaching a terminate step ends the workflow immediately (no routes evaluated after).status: successreturns the rendered output cleanly (CLI exit 0, dashboard ✅, emitsworkflow_completed { termination_reason, terminated_by, is_explicit: true, status: "success" });status: failedraises a newWorkflowTerminatedexception (ExecutionErrorsubclass), gives the CLI a non-zero exit code while still printing the rendered output JSON to stdout for downstream tooling, and intentionally skips the on-failure checkpoint save because explicit termination is not a resumable transient failure. Inside a sub-workflow, a failed terminate is downgraded at the parent boundary to a newSubworkflowTerminatedError(also anExecutionError) preserving the child's renderedterminated_output/terminated_reason/terminated_byas structured attributes, so the parent treats it as a normal sub-workflow failure (its ownworkflow_faileddoes NOT inheritis_explicit: true) while debugging surfaces can still inspect what the child intended to emit. Schema validation rejectsroutes,tools,output,prompt,model,provider, and the other agent-only fields on terminate steps, and conversely rejectsstatus/reason/output_templateon every other step type so authors who forgettype: terminateget a clear error instead of silently dropped fields. Terminate cannot be used as a parallel-group member or as afor_eachinline agent — route to one from those groups'routes:instead. The example workflow lives atexamples/terminate.yaml(#219). runtime.providernow accepts either the bare string shorthand (provider: copilot) or a structuredProviderSettingsobject that forwards aProviderConfigto the Copilot SDK'screate_session(provider=…)parameter. This lets workflows route the Copilot SDK at OpenAI-compatible / Azure / Anthropic endpoints — Ollama, vLLM, LM Studio, Azure OpenAI, llamafile, or any other OpenAI-compatible REST endpoint — instead of being locked to the GitHub Copilot service. The structured form supportsname,type(openai|azure|anthropic),wire_api(completions|responses),base_url,api_key,bearer_token,headers, andazure.api_version.api_keyandbearer_tokenare PydanticSecretStr(redacted inmodel_dump, dashboard payloads, event logs, and checkpoints). Custom routing activates only when YAML sets at least one non-namefield — ambientOPENAI_*env vars never divert default routing on their own. Once activated, missing fields fall back fromCOPILOT_PROVIDER_BASE_URL→OPENAI_BASE_URLforbase_url,COPILOT_PROVIDER_API_KEYforapi_key, andCOPILOT_PROVIDER_BEARER_TOKENforbearer_token. AmbientOPENAI_API_KEYis intentionally NOT consulted as an implicit fallback (credential-leak risk); useapi_key: ${OPENAI_API_KEY}YAML interpolation for explicit opt-in. The schema rejects every non-namefield whenname != "copilot"(structured config for Claude / openai-agents is a follow-up), and rejects anchorless or empty combinations (wire_api/type/headers/azurealone, emptyheaders, emptySecretStr, emptyazureblock) so silent no-ops cannot reach the SDK. Custom routing applies to both agent execution and dialog turns so all sessions hit the same endpoint. Seeexamples/copilot-local-llm.yamland Configuration → Custom Provider Routing (#225, #136).
- New
output_modefield onAgentDef(raw|envelope). Settingoutput_mode: rawbypasses JSON schema injection and parse-recovery entirely, wrapping the model's response as{"result": "<text>"}. Useful for agents that produce large Markdown, prose, or code output that should not be JSON-extracted.output_mode: rawis incompatible withoutput:— declaring both raises aValidationErrorat config load time. - New
max_parse_recovery_attemptsfield onRetryPolicy(YAMLretry:block, per-agent or workflow-level). Overrides the provider default (Copilot: 5, Claude: 2) for agents that need tighter or looser in-session parse-recovery budgets. Accepts integer 0–10;0disables all recovery attempts and lets the first parse failure propagate immediately. Threaded through both the Copilot and Claude providers. - New
POST /api/gate-respondandGET /api/gate-statusHTTP API endpoints on the web dashboard server.GET /api/gate-statusreturns whether ahuman_gateagent is currently waiting, and which agent name it is.POST /api/gate-respondresolves the parked gate by injecting aGateResponseinto the engine's queue. When the optionalCONDUCTOR_GATE_TOKENsecret is configured on the server,POST /api/gate-respondrequires anAuthorization: Bearer <token>header matching it (compared in constant time) — requests with a missing or mismatched token are rejected with HTTP 403.GET /api/gate-statusis unauthenticated. The matching WebSocketgate_responsepath enforces the same token and waiting-state checks so it cannot be used to bypass auth. - New
conductor gate-respondCLI command for resolving a parked human gate from the command line without opening a browser. Accepts--port,--choice,--agent(auto-discovered via/api/gate-statuswhen omitted),--input, and--token/CONDUCTOR_GATE_TOKENenv var. Designed for SSH or headless environments where the web dashboard UI is unreachable. scriptsteps now resolve a bare command name (e.g.python) or an extension-less path against the executable search path before launching, so the binary the shell would pick is the one that runs (and a Windows path missing its.exe/.cmdsuffix resolves correctly). Resolution uses the subprocess's ownPATH— including anyenv.PATHoverride on the step — so the resolved binary matches what the child process would execute. Relative paths containing a separator are left untouched so they keep resolving againstworking_dir, and an unresolvable command falls back to the rendered value so the existing not-found error still fires.
- Breaking (Claude provider):
ClaudeProvider._extract_text_contentnow returns{"result": "<text>"}instead of{"text": "<text>"}. This aligns the Claude provider with the Copilot provider (cross-provider parity). Any existing Claude workflow that references{{ <agent>.output.text }}must be updated to{{ <agent>.output.result }}. Workflows that declare anoutput:schema are unaffected (the schema fields take precedence). See the newoutput_mode: rawfeature if you need to consume unstructured text output reliably across both providers.
_verbose_consoleis now silent-aware at the source: a_SilentAwareConsolesubclass no-ops every.print(...)whenis_verbose()is False, so the remainingconductor --silentstderr leaks (dashboard-failed-to-start and log-file-open warnings, workflow-hash mismatch, "Press Esc to interrupt", "Event log written to…", "Log written to…",_print_resume_instructions, and the replay command's "Press Ctrl+C to exit" / "Replay stopped" banners) no longer reach stderr. The app-wideconsoleremains unchanged because it carries real error messages; the two replay prints are gated per-call.conductor --silent replay <log>now produces zero bytes on stderr (#223, closes #209).- Parse-exhaustion
ProviderError(after all in-session recovery attempts are spent) is now markedis_retryable=Falsein both Copilot and Claude providers. Previously Copilot marked itis_retryable=True, causing the outer retry loop to re-run the entire agent up to 3× on deterministic parse failures — burning tokens with no chance of success. - Parse-exhaustion error messages now include the first 500 characters of the
model's response (up from 200) and suggest
output_mode: rawas a fix. parse_json_outputand the Copilot provider's_extract_jsonnow use a two-stage fenced-block extraction (non-greedyre.findall+ per-candidate try-parse, then a greedy single-capture fallback) so JSON whose string fields contain triple-backtick substrings no longer matches prematurely and falls into parse-recovery loops, while responses with multiple fenced JSON blocks still pick the first valid one. Resolves a recurring failure mode for agents emitting Markdown-bearing JSON (external-workflow-friction Issue #1) (#232).conductor run --web-bgandconductor resume --web-bgnow abort before forking when the workflow contains ahuman_gateagent (including gates nested infor_each.agent) and--skip-gatesis not set, with a message listing the four supported options.resume --web-bgalso recovers the workflow path from the checkpoint when invoked without an explicit workflow argument so the guard still fires. Previously the detached child crashed withEOFErrorand the parent only reported "Background process exited immediately with code 1" (Issue #8).
- New "Choosing whether to declare
output:" section in docs/workflow-syntax.md describing when to declare a schema versus consuming raw<agent>.output.resultfor prose or large JSON. Closes a documentation gap that contributed to misconfiguration of agents emitting large payloads (Issue #2). docs/cli-reference.md--web-bgsection now documents thehuman_gateincompatibility and the new pre-fork validation behavior.
- Workflow
limits.budget_usdandlimits.budget_mode(audit|enforce) cap cumulative LLM cost across a run.audit(default) emits abudget_exceededevent and continues so users can profile costs before enforcing;enforcesaves a checkpoint and stops withBudgetExceededError. Resuming withconductor resumestarts a fresh budget window (cumulative spend resets to $0), so the remaining work runs under a full budget — raisingbudget_usdfirst is optional. Sub-workflow spend is merged into the parent so a parent budget accounts for delegated cost. Schema, engine enforcement at all five existing limit-check points, resume parity for restored budget state, and the newBudgetExceededErrortype are wired end-to-end. See docs/workflow-syntax.md and docs/configuration.md for the graduation path.
0.1.17 - 2026-05-21
- Script agents can now declare an
output:schema using the same OutputField syntax as LLM agents. When declared, the engine parses stdout as JSON and validates it against the schema before emittingscript_completed; missing fields, wrong types, non-JSON stdout, empty stdout, and JSON arrays/scalars all raiseValidationErrorand emitscript_failed(with stdout/stderr/exit_code) instead of completing. Validation runs on the merged output dict so declaredstdout/stderr/exit_codefields validate the value downstream actually sees (matching the PR #122 shadowing contract). An explicitoutput: {}opts into strict JSON-object mode with zero required fields. Without a declared schema, the legacy best-effort JSON-stdout auto-merge from PR #122 is fully preserved, so this is purely additive. Routing conditions can now reference declared fields (e.g.when: "phase == 'planning'") rather than opaque exit codes (#206, #118). conductor validatenow warns on undeclaredagent.outputreferences and field-level mismatches inexplicitcontext mode, closing two follow-up gaps left by PR #125 that still produced the runtimeTemplateError: 'dict object' has no attribute 'X'from issue #105. The validator now tracks declared fields per agent root (a.output.foovsa.output.bar), so a prompt that references an undeclared field on an otherwise-declared agent surfaces a warning instead of a runtime failure; the same logic applies to static parallel groups (pg.outputs.member.field). Output-vs-error namespaces are tracked independently soinput: ["pg.errors"]no longer silently suppresses warnings for{{ pg.outputs.* }}references, and the AST walker now filters inner-linkGetattrnodes (no more spurious whole-output refs from{{ a.output.bar }}chains), detects method-call nodes ({% for k,v in a.output.items() %}registers as a whole-output ref), and degrades gracefully onTemplateAssertionError. For-each groups remain skipped (whole-member copy makes field precision a false positive);human_gateis now correctly excluded fromagent.outputwarnings since the engine renders gate prompts in accumulate mode (#208, refs #105).
- Copilot provider verbose log lines (tool calls, reasoning, processing
indicators, idle/parse recovery) are now prefixed with the originating
agent name in parallel and for-each runs, eliminating the
un-attributable interleaved output that made the for-each case
unreadable (every iteration previously shared the same agent name).
An optional
agent_nameparameter is plumbed through_execute_sdk_call→_send_and_wait→_log_event_verboseand rendered as a magenta[agent_name]tag between the tree icon and event content (continuation lines tagged too). For-each iterations additionally get amodel_copy()of the per-iteration agent withname = f"{name}[{key}]"so each iteration produces a distinct tag; the originalAgentDefis untouched and context lookups still use the unqualified name. Static parallel groups are unaffected — each agent already has a unique name. The_item_callbackmerge order is flipped so the wrapper'sagent_name/item_keywin over any qualified name the provider emits, preserving the dashboard/JSONL event contract (agent_name= for-each group name;item_keydisambiguates iterations). Backward compatible:agent_namedefaults toNonefor sequential agents (#207, closes #16).
-
conductor resume … --weband--web-bgno longer open an empty dashboard. Checkpoints now record the originalrun_idand JSONLevent_log_path. On resume the dashboard's history is seeded BEFORE it accepts clients: the CLI prepends a freshworkflow_startedevent built from the current YAML (so historical events apply to the correct topology), then replays the original JSONL log line-by-line (or, when no log file is available, synthesises minimal*_started/*_completedpairs from the restored execution history). The resumed engine's ownworkflow_startedemit is suppressed so the dashboard sees exactly one root start — nowfDepthdouble-counting. Root-levelworkflow_completed/workflow_failed/checkpoint_savedevents from the original run are filtered out on replay; subworkflow lifecycle events are preserved so the frontend's context tracking stays balanced. The resumedEventLogSubscriberappends to the original log, preservingrun_idacross resume generations so log/timeline correlation tools see one continuous run (#167). -
--web-bgstartup crashes on Windows are no longer silent (#116). Three changes work together to make any crash forensically traceable:conductor.cli.bg_runnernow captures the detached child's stdout and stderr to log files in$TMPDIR/conductor/(named to match the existing.events.jsonlfilename) instead of discarding them withsubprocess.DEVNULL. A Python traceback orfaulthandlerdump from the child now survives the parent's exit. The captured stderr path is printed alongside the dashboard URL and is included in every background-launch failure message so users always know where to look.conductor/__init__.pyenablesfaulthandlerat import time (writing tosys.__stderr__), so a native crash — segfault, abort, fatal Python error — dumps a Python-level stack trace into the captured stderr log.WorkflowEngine._execute_loopnow catchesBaseException(in addition to the existingKeyboardInterrupt/ConductorError/Exceptionarms) and emits aworkflow_failedevent withis_base_exception: truebefore re-raising. A bareSystemExitor other non-Exceptionfailure betweenagent_startedandagent_prompt_renderednow leaves a structured failure event in the JSONL log instead of an unexplained two-event truncation. An explicitexcept asyncio.CancelledError: raisearm sits in front of it so a normal dashboard-stop or parent cancellation is not mis-reported as an unexpected failure. Two new env vars (CONDUCTOR_RUN_ID,CONDUCTOR_BG_STDERR_LOG,CONDUCTOR_BG_STDOUT_LOG) propagate the parent-chosen run id and log paths to the child so the bg log files and the child's events JSONL share an 8-hex run id in their filenames, andworkflow_startedsystem metadata surfaces both bg log paths to the dashboard. The root cause of the underlying intermittent Windows crash is still pending — this change makes it diagnosable rather than invisible.
-
Workflows that configure
reasoning.effort(or workflow-wideruntime.default_reasoning_effort) on the Copilot provider were broken for every named Copilot model when running againstgithub-copilot-sdk0.3.0. The SDK'smodels.listresponse includes abillingobject on every model, but none of them currently ship themultiplierfield that the SDK'sModelBilling.from_dictparser treats as required — so every model in the response triggersValueError("Missing required field 'multiplier' in ModelBilling"), which kills the entirelist_models()call. The error then leaked through the narrowexcepttuple in_validate_reasoning_effort_for_model(andget_max_prompt_tokens), poisoned the retry loop, and surfaced asDialog turn failed: …after three wasted attempts. (get_max_prompt_tokenswas rescued by the engine's outerexcept Exception, so context-window metadata was silently unavailable rather than fatal.) Both metadata methods now catch anyExceptionraised at the SDK boundary and treat the failure as "metadata unavailable" — validation is skipped permissively and the configuredreasoning_effortis forwarded tocreate_sessionas before.asyncio.CancelledError/KeyboardInterrupt/SystemExit(allBaseExceptionsubclasses) still propagate. -
conductor resume --web-bg(and--web) no longer exit silently when a workflow exceedsmax_iterations. The bg child was forked with--no-interactiveandstdin=subprocess.DEVNULL, so when the engine hit the limit,IntPrompt.askraisedEOFError, got coerced to0(stop), and the workflow ended with no way to recover. The max-iterations gate can now be resolved from the dashboard. New resolution policy:skip_gatesauto-stops (unchanged); no web dashboard uses the legacy CLI prompt (unchanged); web dashboard + bg/non-TTY stdin uses a web-only wait (the CLI prompt is deliberately NOT raced because it would synchronouslyEOFErrorand win every dashboard click), withdashboard.wait_for_stop()racing soPOST /api/stopcan terminate the wait when no dashboard tab is open; web dashboard + TTY foreground races CLI vs web. Eachiteration_limit_reachedpayload carries a uuid4gate_idthat the dashboard must echo back initeration_limit_response, and the server matches/discards stale responses so a delayed double-click cannot be misapplied to a later gate.iteration_limit_resolvedincludes the samegate_idso subscribers can correlate the pair. New top-levelIterationLimitModal(parallel-group gates can't attach to a per-agent panel) shows iteration count, recent agent history, and number input; it is hidden whenskip_gatesis true and does not close on Escape so the workflow can't be accidentally orphaned (#202, fixes #198). -
conductor run --web-bgandconductor resume --web-bgno longer get killed within ~10 seconds when launched from a shell wrapper that runs commands inside a Windows job object withJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE(GitHub Actions runners, VS Code integrated terminal, JetBrains IDE terminals, GitHub Copilot CLI shell tool). The detached child previously inherited the parent's job and died with it; users saw a dashboard URL but the workflow never made progress. ThePopencall now requestsCREATE_BREAKAWAY_FROM_JOBin addition toCREATE_NEW_PROCESS_GROUPso the child fully detaches. In hardened CI environments that clearJOB_OBJECT_LIMIT_BREAKAWAY_OK,CreateProcessraisesERROR_ACCESS_DENIED; in that case a visible stderr warning is emitted (so the user understands bg mode may not survive shell exit) and the spawn is retried without the breakaway flag. OtherOSErrors propagate unchanged so the existingRuntimeErrorwrapper still surfaces them cleanly. Refactors the two near-identical detachment+Popen blocks inlaunch_backgroundandlaunch_background_resumeinto a single_spawn_detachedhelper; constants are resolved viagetattrso the module remains importable on POSIX hosts and tests can patchsys.platformto"win32"from Linux/macOS (#200). -
conductor run --web-bg --log-file autonow produces a log file with a real provider-side trace.bg_runner.launch_background()/launch_background_resume()already redirect the child's stdout/stderr/stdin tosubprocess.DEVNULL, so silence is enforced at the OS level — but they also passed--silentto the child, which flippedverbose_mode=Falseand gated more than console prints (the Copilot provider's_log_event_verbose(),_log_parse_recovery(), and_log_recovery_attempt()all became no-ops, dropping events from the log file too). Both synthesized commands now omit--silent; console output still goes to DEVNULL via the Popen kwargs. Side benefit: the synthesized command is now reproducible by hand without learning that--silentwas being injected behind the scenes (#199, #196). -
--web-bgand other--silentinvocations no longer leak the dashboard URL banner to stdout. Severalconsole.print/typer.echocalls incli/run.pywere unconditionally writing the bg-launch URL, stderr log path, andconductor stophint even with--silent/is_verbose() == False. Remaining unguarded URL prints are now gated behindis_verbose()so--silentis honored end to end (#203, #211). -
conductor validate <registry-workflow>now succeeds for workflows thatconductor runalready executed successfully. The validator's_resolve_subworkflow_ref_for_validationwas missing the step that the engine's_resolve_subworkflow_pathalready had: when a parent workflow lives inside a registry SHA cache and references a sibling via a relative path (e.g.../document-review/workflow.yaml), the engine auto-fetches the sibling from the same registry+SHA cache viaauto_fetch_relative_workflow. The validator only checked the filesystem and reported "sub-workflow file not found". Validation and execution now agree on which refs are resolvable (#197). -
Registry cache now mirrors the source repository layout so repo-relative references between workflows in the same registry repo resolve correctly. Previously each workflow was isolated under
<base>/<registry>/<workflow_name>/<sha[:12]>/<filename>, sosdd-plan/plan.yamlreferencing../document-review/workflow.yamlresolved to a path that never existed in the cache and forced manual workarounds. The cache now stores workflows from the same registry+SHA under a shared per-SHA root (<base>/<registry>/<sha[:12]>/<repo_path>); metadata lives in a sibling_meta/<sha[:12]>/tree so it can never collide with real repo paths (e.g. a repo's own.conductor/directory). Per-workflow readiness sentinels are written last so readers never observe a partially populated workflow; per-fileos.replace()stays intra-filesystem for atomic promotion;_safe_repo_path()rejects.., absolute paths, NUL bytes, and empty paths from any index/sibling entry;_resolve_within()adds defense-in-depth that resolved targets stay under the SHA root;source.jsoncarriescache_layout_version,registry_type,source, andfull_shaso cache hits require all four to match (stale metadata triggers re-fetch); the registry index is cached on disk so cache hits avoid a network round-trip. Sub-workflow refs from the same registry are auto-fetched when not yet present (gated to file-path-looking candidates with no@).add_registry()now rejects names containing/,\, the empty string, or the reserved_adhoc/_metanamespaces (#194).
0.1.16 - 2026-05-14
type: workflowagents now accept registry references (workflow[@registry][#ref]) in theworkflow:field, not just local file paths. Resolution prefers a local file when one exists relative to the parent workflow directory (preserves backward compatibility for extensionless local refs); otherwise the value is parsed as a registry reference, fetched via the registry cache, and executed from the cached location.conductor validatenow recursively validates fetched sub-workflows with cycle detection (inode-based identity, so case-variant paths on macOS/Windows collapse correctly) and a depth cap of 10 — when the cap is hit a warning surfaces so users know validation was truncated rather than silently clean. Mutable registry refs (name@registry#main, or no#ref) may resolve to a different commit onconductor resumeif the upstream branch has moved; pinned tags or commit SHAs guarantee deterministic resume (#188).- Conductor now ships as a Claude Code plugin marketplace at the repo root.
Users can install the conductor skill directly from
microsoft/conductorwith/plugin marketplace add microsoft/conductorfollowed by/plugin install conductor@conductor. The plugin ships markdown only (nobin/, hooks, MCP servers, or executables), keeping the trust surface minimal. The sameSKILL.mdremains usable viagh skill install microsoft/conductor conductorfor Copilot CLI users. The previous.claude/skills/conductorlocation was removed — the plugin is now the single home for the skill; for local development on the skill itself, useclaude --plugin-dir plugins/conductor(#186).
- The bundled Conductor skill (
SKILL.md+ references) was refreshed to reflect the current CLI, schema, and feature set:show/replay/--metadata/--workspace-instructionsquick-reference entries; newtype: workflow,dialog,retry,hooks,metadata,instructions,timeout_seconds, andopenai-agentsprovider concepts; correctedupdatebehavior (default prints the install-script one-liner,--applylaunches the installer);CONDUCTOR_NO_UPDATE_CHECK; registrylatest = branch HEADand#refsyntax; sub-workflow agents and dialog mode authoring guidance; script JSON-stdout auto-merge;workflow.dir/workflow.filetemplate variables; and unknown-fields rejection in schema validation (#187). - README "Why Conductor?" rewritten around three pillars — repeatable execution, deterministic routing, and version-controlled YAML workflows — and now leads with the real differentiator (zero-token orchestration) using concrete use-case examples (#185).
0.1.15 - 2026-05-13
- Per-agent
timeout_secondsfield for hard wall-clock timeouts on agent execution. Wraps execution inasyncio.wait_for()at the engine level so a slow agent no longer blocks the rest of the workflow. Effective timeout ismin(agent.timeout_seconds, remaining_workflow_timeout)— when the workflow timeout is stricter it owns the error so attribution is never mislabeled. Raises a newAgentTimeoutError(subclass ofTimeoutError) honored by existingfail_fast/continue_on_errorsemantics in parallel and for-each groups, and emits anagent_timeoutevent (with elapsed time and limit) for console + dashboard subscribers. Scoped to provider-backed agents; rejected onscript,human_gate, andworkflowtypes (#150). - Auto-discovery of
.github/instructions/**/*.instructions.mdworkspace conventions, matching GitHub Copilot's documented semantics. Files markedapplyTo: "**"in their frontmatter are loaded into the workspace preamble alongsideAGENTS.md/CLAUDE.md/.github/copilot-instructions.md; scoped (applyTo: "<glob>") and absent-applyTofiles are skipped per the convention's manual-attach default. The internalCONVENTION_FILES: list[str]table is refactored to a polymorphicCONVENTIONS: list[Convention](ConventionFile | ConventionDirectory) so adding new conventions (Cursor rules, Cline rules, etc.) becomes one filter function plus one list entry; aCONVENTION_FILESmodule-level alias preserves backward compatibility for downstream imports (#169).
agent.system_promptis now rendered and forwarded to providers. The executor was renderingagent.system_promptonly to discard the result (_ = self.renderer.render(...)), so providers that forwardsystem_prompt— notably the Copilot provider, which concatenates it into the prompt — received the un-rendered Jinja template. Agents whose instructions lived insystem_promptsent literal{{ ... }}placeholders to the model and got back "the prompt template contains unfilled variables" refusals. Also adds aconductor validatewarning for agents that definesystem_promptbut noprompt:(a portability hazard since the Claude provider dropssystem_promptentirely, and almost always a missing-prompt:typo) (#179).conductor updateon Windows no longer attempts an in-process self-upgrade. The previous flow tried to re-install into the same venv the runningpython.exelives in, producing "Access is denied" failures that earlier mitigations only papered over.conductor updatenow checks for a newer version and prints the OS-appropriateinstall.ps1/install.shone-liner, and the install scripts become the single upgrade path: they detect other running conductor processes (auto-stopping under-Yes), sweep stale*.exe.oldfiles, retry with backoff (2s / 5s / 10s), and — when uv can't remove theconductor-clitool dir because of file locks — rename the whole dir aside and retry.install.shreaches parity with--yes/--force/--sourceflags, retry-with-backoff, running-process detection, and a post-installconductor --versionverify (#171).install.ps1is now stored without a UTF-8 BOM. The documented one-linerirm https://aka.ms/conductor/install.ps1 | iexreturns the script body as a single string with the BOM surviving as U+FEFF at index 0; PowerShell's in-memoryiexparser then trips on the[CmdletBinding()]attribute withUnexpected attribute 'CmdletBinding'. Both fresh installs viairm | iexandconductor update --apply(which re-runs the same command in a spawned console) now succeed. Directpowershell.exe -File install.ps1invocations were unaffected, which is why prior file-based integration tests didn't catch it (#178).conductor stop(including--alland--port) no longer crashes on Windows when a PID file exists in~/.conductor/runs/. The Unix idiomos.kill(pid, 0)for liveness probing is not a no-op on Windows — any signal other thanCTRL_C_EVENT/CTRL_BREAK_EVENTroutes throughTerminateProcessand can raiseOSErrorsubclasses outsideProcessLookupError/PermissionError(e.g.WinError 11 / ERROR_BAD_FORMAT), and even "successful" calls would actually terminate the target with exit code 0._is_process_alive()now dispatches to a Windows-specific implementation usingOpenProcess+GetExitCodeProcessfor a truly non-destructive liveness check (#176).
0.1.14 - 2026-05-06
conductor updateno longer reports its own launching shim as another running Conductor process. On Windows theconductor.exeshim is a separate process from the Python interpreter that runs the update command, so excluding onlyos.getpid()caused a false "1 other Conductor process is running" warning. The check now walks the full ancestor PID chain (viawmicon Windows,pselsewhere) and excludes every process along the way, falling back to{getpid(), getppid()}if the parent map cannot be built. #164
0.1.13 - 2026-05-06
conductor resumeis now at flag parity withconductor run. New flags:--provider/-p(runtime provider override),--metadata/-m(CLI metadata merged on top of YAML metadata),--web(real-time dashboard for the resumed run),--web-port, and--web-bg(fork a detached resume + dashboard process).--weband--web-bgare mutually exclusive, matchingrun. 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.--input,--workspace-instructions,--instructions, and--dry-runare intentionally not mirrored (#158).- Reasoning effort (
low/medium/high/xhigh) is now displayed in the web dashboard under each agent's metadata, right afterModel. Effective value is per-agentreasoning.effortif set, otherwiseruntime.default_reasoning_effort, otherwise omitted. Backed by a newreasoning_effortfield on theworkflow_startedevent payload, so older event log JSONL files replay gracefully (the row simply doesn't render) (#160). - New
iteration_limit_reachedanditeration_limit_resolvedevents are emitted when a workflow hits itsmax_iterationscap. Previously the console showed an interactiveIntPromptwhile the web dashboard went silently dark; the dashboard now renders the prompt state and the chosen resolution. Theiteration_limit_reachedpayload includes apossible_loopheuristic flag (set when the last 3 history entries are the same agent) so subscribers can call out stuck review loops (#162).
- Workflow registry references now resolve
latest(and barename@registryrefs) to the default branch HEAD instead of the newest git tag. Previously, the moment a registry repo got its first tag, bare references silently froze at that tag and stopped picking up commits tomain. Tags remain first-class — pin explicitly viaworkflow#v1.2.3for releases. Also saves one GitHub API call on the hot path of bare-name fetches (#157).
- Schema validation now rejects unknown fields on
AgentDef,ParallelGroup,ForEachDef, andWorkflowConfiginstead of silently dropping them. Misnestingparallel:orfor_each:inside anagents:item — or typos likeprmpt:— used to fall through to a runtimeModel "gpt-4o" is not availableerror three layers downstream. They now fail at parse time with a clear Pydantic error pointing at the offending location.conductor validatealso gained "Parallel Groups" and "For-each Groups" rows in its summary table so missing groups are immediately visible (#159). - Tool arguments and results are now pretty-printed in dashboard / JSONL /
verbose-console events. Copilot tool results no longer leak the full
Result(content=..., contents=None, detailed_content=..., kind=None)repr with literal\\nescapes and doubled\\\\Windows paths, and tool arguments render as JSON ({"k": "v"}) instead of Python dict repr ({'k': 'v'}). Both providers share a newsrc/conductor/providers/_event_format.pyhelper for parity (#161). install.ps1on Windows now captures fulluv tool installstdout AND stderr viaStart-Process -RedirectStandardOutput -RedirectStandardErrorto temp files. Previously, with$ErrorActionPreference = 'Stop', PowerShell treated uv's stderr as a terminating error and threw before the assignment completed, so install failures showed(no output captured)with no way to diagnose them (#156).
0.1.12 - 2026-05-05
- Unified
reasoning.effortconfiguration for per-agent and workflow-wide control of model reasoning / extended-thinking effort. Setruntime.default_reasoning_effort(low|medium|high|xhigh) for a workflow-wide default, or override per agent with areasoning.effortblock. Translates toreasoning_efforton the Copilot session and to extendedthinkingbudget on Claude (low=2048, medium=8192, high=16384, xhigh=32768 tokens, withtemperaturecoerced to 1.0 andmax_tokensbumped to fit). Validates against each model's supported efforts/capabilities and surfaces thinking content viaagent_reasoningevents. Seeexamples/reasoning-effort.yaml(#152). - Tag-based versioning for the workflow registry. Versions are now
auto-discovered from git tags instead of being explicitly listed in
registry.yaml, and refs accept any tag, branch, or SHA via the newworkflow#refsyntax (e.g.sdd/plan#v3.0.0,sdd/plan#main,sdd/plan#abc1234). Stale CDN content is bypassed via cache-busting query parameters so registry updates are visible immediately (#151).
conductor updatereliability on Windows. Adds a pre-flight check for other running Conductor processes (which hold file locks on%LOCALAPPDATA%\uv\tools\conductor-cli\and causeuv tool install --forceto fail with "Access is denied"), retries the install up to 3 times to absorb transient Windows Defender failures, surfaces full uv stdout AND stderr on failure with Defender-exclusion guidance, broadens the Windows entrypoint rename to cover the uv tool venvScripts/directory in%LOCALAPPDATA%and%APPDATA%, and adds a newconductor update --forceflag to skip the pre-flight check (#155).- Dashboard layout for workflows with
human_gateoptions or multiple loop-back routes (e.g. revision loops). Theworkflow_startedevent now emits routes fromhuman_gateoptions[].routeso gate edges aren't silently dropped, and the frontend pre-classifies back-edges via DFS from$startand feeds them to Dagre in reversed direction so cycles no longer scramble rank assignment. Workflows likesdd/plan-v3.yamlnow render as a coherent top-to-bottom DAG instead of disconnected columns with long diagonal edges (#153). - Windows install failures now surface useful diagnostics.
install.ps1prints captureduvstdout/stderr on failure instead of swallowing it, and uses the correct Microsoft Defender cmdlet so the install path is exclusion-friendly (#149).
0.1.11 - 2026-05-04
metadatadict on workflow definitions, settable statically in YAML or dynamically via--metadata/-mCLI flags. Merged metadata is included in theworkflow_startedevent for downstream consumers (#107).input_mappingfield ontype: workflowagents, enabling Jinja2-templated per-call inputs to sub-workflows evaluated against the parent context. When omitted, the parent'sworkflow.input.*is forwarded as before (#109).type: workflowagents are now allowed insidefor_eachgroups, enabling dynamic fan-out to sub-workflows with per-iterationinput_mapping. Each iteration emits its ownsubworkflow_started/subworkflow_completedevents (#110).- Self-referential sub-workflows are now allowed; depth is bounded by the
global
MAX_SUBWORKFLOW_DEPTHplus an optional per-agentmax_depthfield onAgentDef(#111). workflow.dir,workflow.file, andworkflow.nametemplate variables are now available in all agent contexts (regardless of context mode). Lets registry-hosted workflows reference co-located scripts and assets without depending on the caller's working directory (#121).- Script agent stdout that is valid JSON is auto-parsed and merged into
the agent's output dict alongside
stdout,stderr, andexit_code, enabling field-basedwhen:route conditions instead of opaque exit-code matching (#122). conductor validatenow performs semantic validation in addition to YAML schema checks, catching stale agent references, missing workflow inputs, and undeclared explicit-mode dependencies before runtime inprompt,system_prompt,command,args,working_dir,input_mapping, parallel-group inputs, and workflowoutput:templates (#125).- Web dashboard: breadcrumb navigation, double-click dive-in to sub-workflow graphs, isolated subworkflow contexts (no node-status bleed across repeated runs), and reliable Stop button during subworkflows (#113, follow-up fixes in #146).
- Dialog mode for agents: multi-turn conversational interactions
driven by a
dialoggate with conditional transitions, full Copilot and Claude provider support, and dedicated dashboard UI (DialogDetail,DialogEngagementPrompt,DialogOverlay) (#130). - Markdown rendering and auto-linkification in human gate prompts.
Gate prompts render through Rich Markdown in the terminal and as
GitHub-Flavored Markdown in the dashboard. Bare file paths and URLs
in gate prompts are converted to clickable links; relative paths
open a sandboxed
FileViewermodal served via a path-traversal-safeGET /api/files/{path}endpoint (#131). - Workspace instructions support:
--workspace-instructionsand--instructionsCLI flags plus a YAML-levelinstructions:field on the workflow. Auto-discoversAGENTS.md,CLAUDE.md, and.github/copilot-instructions.mdby walking from CWD to the git root, prepends them to every agent's prompt, inherits into sub-workflows, and persists in checkpoints (#141).
- The dashboard's "context window remaining" bar now sources
context_window_maxfrom each provider's SDK at runtime instead of a hand-maintained static table. Values now reflect the actual cap the SDK enforces (e.g.claude-opus-4.6reports 200K rather than the theoretical 1M;gpt-5.xreports 128K rather than 400K). Thecontext_windowfield onModelPricinghas been removed; pricing data continues to be hand-maintained for cost calculation only (#144).
- Pass
streaming=Trueto the Copilot SDK'screate_sessionto prevent silent truncation of large tool-call arguments. In non-streaming mode the model's per-turn output budget is exhausted mid-JSON for large arguments (e.g.,createwith multi-KBfile_text), the CLI executes the partial tool call, and the agent loops on the broken call until the wall-clock session limit fires (#129). - Build the Copilot prompt schema recursively from nested
output:definitions instead of flattening to top-level fields only. Nested object properties, required keys, and array item schemas are now included in the prompt-facing schema used for initial guidance and parse recovery (#100). - Coerce Python literal
"True"/"False"/"None"strings produced by Jinja's defaultstr(bool)rendering into native Python types when building workflow output. Previously,output: { matched: "{{ a == b }}" }produced the string"False"(truthy), causing downstreamwhen:comparisons againstfalseto silently misbehave (#139). - Pricing fuzzy match no longer silently inherits values across model
families. Names sharing a textual prefix with a known key (e.g.
claude-opus-4.7previously matchedclaude-opus-4) now require a-delimiter; non-matching names returnNoneand the dashboard hides the cost field. A one-time warning is emitted per requested name on any non-exact match (#143). - Run
uv tool update-shellafteruv tool installin bothinstall.ps1andinstall.shsoconductoris available on PATH in new shells, CI agents, and IDE extensions after a fresh install (#142). - In explicit context mode,
workflow.inputis now always available toscriptandtype: workflowagent templates regardless of the agent's declaredinput:list. The explicit-mode contract still applies to LLM agents (no undeclared inputs in prompts to control token cost) (#119). - Optional workflow inputs without an explicit
default:now resolve to type-appropriate zero values ("",0,false,[],{}) instead of PythonNone, so templates like{{ workflow.input.optional | default("fallback") }}render the fallback rather than the literal string"None"(#123). - Web dashboard: events without an engine-supplied
subworkflow_pathstamp (e.g.,for_each_item_startedfor a parent for_each overtype: workflowagents) now route strictly to the root context instead of falling back to the user's currently-viewed path. This fixes two related symptoms: dashboards opened during a run with sub-workflows no longer auto-land inside an iteration, and a parent for_each panel now displays every iteration rather than silently dropping the middle ones into a sibling sub-workflow's context (#148).
0.1.10 - 2026-04-30
- Sub-workflow composition support:
workflow-type agents can now be used insidefor_eachgroups, with dynamic per-iterationinput_mapping(#101, #102).
- Bumped
github-copilot-sdkto>=0.3.0. The SDK ships a bundledcopilotCLI binary used for JSON-RPCsession.createcalls;0.2.2bundled CLI1.0.21, which rejected newer model IDs locally withJSON-RPC -32603: Model "<id>" is not available.0.3.0bundles CLI1.0.36-0, which accepts the current Copilot model catalog (includingclaude-opus-4.7*variants).
- Suppressed noisy PowerShell stderr output from
uv tool installduring Windows self-update (#99).