Conversation
…icrosoft#517) Replace the monolithic AgentDef that carried every step type's fields with concrete Pydantic models united by a static StepDef discriminated union (Field(discriminator="type")): - AgentDef is now the provider-backed LLM step only; new public models HumanGateStepDef, QuestionsStepDef, ScriptStepDef, MCPStepDef, WaitStepDef, SetStepDef, TerminateStepDef (no routes) and WorkflowStepDef own exactly the fields meaningful for their kind with extra="forbid", all exported from conductor.config[.schema]. - WorkflowConfig.agents and ForEachDef.agent are typed list[StepDef] / StepDef and parse into concrete variants; an omitted or explicit-null type still loads as an LLM agent and is canonicalized to "agent" at the boundary. The published JSON Schema now exposes oneOf + discriminator with per-variant additionalProperties: false. - Engine main/parallel/for-each dispatch narrows via isinstance with defensive ExecutionError guards for variants the static validator would have rejected (a directly-constructed engine skips it); human_gate is rejected as a for_each inline agent statically and at runtime, including before the empty-source early return. - Generic consumers narrowed accordingly: validator routing graph and subworkflow/template scans, cli run/app/plugin, MCP plan tree, executor skills/plugins helpers, synthetic provider agents. - FileString (!file) preservation restored on human_gate/questions prompts; HumanGateStepDef loses the inert model field; foreign fields on a step now fail with Pydantic's standard extra_forbidden instead of the previous custom "<type> agents cannot have '<field>'" messages (per-variant invariant messages unchanged). Tests migrated to the new contract: concrete constructors for valid steps, model_validate + structural extra_forbidden assertions for foreign fields, custom match= kept only for variant-owned invariants.
- JSON Schema shape: oneOf over all nine variants, full discriminator mapping, additionalProperties: false on every variant $def, TerminateStepDef without routes, ForEachDef.agent exposing the same union. - ForEachDef.agent parsing for agent/workflow/set/mcp inline variants. - Legacy compatibility: YAML without type, YAML with type: null, direct AgentDef(type=None), unknown type as union_tag_invalid, and model_dump(exclude_none=True) round-trips for every variant. - Resume round-trip driven by a legacy no-type YAML file. - Runtime defensive guards: unsupported variants in parallel and for-each groups raise ExecutionError (not AttributeError), including forbidden inline agents ahead of the empty-source early return. - conductor_plan_tree renders a terminate step with empty routes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #517.
What
Replaces the monolithic
AgentDef— one model carrying every step type's fields, with a hand-maintained validator matrix deciding which fields eachtype:accepts — with concrete Pydantic models united by a staticStepDefdiscriminated union:AgentDefis now the provider-backed LLM step only.HumanGateStepDef,QuestionsStepDef,ScriptStepDef,MCPStepDef,WaitStepDef,SetStepDef,TerminateStepDef(deliberately noroutes),WorkflowStepDef, plusStepBase/RoutableStepBase. All exported fromconductor.configandconductor.config.schema.WorkflowConfig.agents: list[StepDef]andForEachDef.agent: StepDefparse into concrete variants throughField(discriminator="type")with an outerBeforeValidatorthat canonicalizes the legacy shorthand (missing or explicit-nulltype) to"agent"at the boundary.extra="forbid", so a misplaced field is rejected next to its step instead of being silently ignored — this is what makes new step types cheap to add (no validator matrix to extend).oneOf+discriminatorwith per-variantadditionalProperties: false, so editors and external tooling get per-type completion and validation.Compatibility
typeandtype: nullstill load as LLM agents (canonicalized totype: "agent"at parse time).AgentDef(...)keeps working for LLM agents, includingAgentDef(type=None, ...),model_dump(exclude_none=True)round-trips,model_copy(update=...), and the synthetic agents built byOutputValidatorand the ACA runner.AgentDef(type="script", ...)no longer works (use the named step class); a field belonging to a different step type now fails with Pydantic's standardextra_forbiddenrather than the custom"<type> agents cannot have '<field>'"messages (variant-owned invariant messages are unchanged);HumanGateStepDeflost the inertmodelfield;human_gateis rejected as afor_eachinline agent (concurrent iterations would compete for one interactive channel — route to a gate from the group'sroutes:instead).Engine and consumers
isinstance, with defensiveExecutionErrorguards for variants the static validator would reject — a directly-constructed engine never runs it. The for-each guard fires before the empty-source early return so a forbidden inline variant can never pass silently.conductor show/run/plugin, the MCPconductor_plan_tree(a terminate step now renders with empty routes), executor skills/plugins helpers, and the synthetic provider agents (max_depthremoved).FileString(!file) preservation restored onhuman_gate/questionsprompts so file-backed gate prompts keep resolving relative{% include %}paths.Testing
mainin this environment (twochmod(0o000)permission tests and one timing-sensitive performance test); none touch this change.ruff checkclean;ty check srcshows only the same pre-existing diagnostics asmain;make validate-examplesgreen.oneOf/discriminator/additionalPropertiesshape,ForEachDef.agentvariant parsing, legacy missing/null-typecanonicalization,model_dumpround-trips for all nine variants, resume round-trip from a legacy no-type YAML, runtime defensive guards for parallel/for-each groups (including ahead of the empty-source early return),conductor_plan_treewith a terminate step, and an explicit-emptyoutput_template: {}coverage-override regression.