Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Output field constraints — `enum`, `pattern`, `minimum`/`maximum`,
`minLength`/`maxLength`, `required`, `nullable`**
([#372](https://github.com/microsoft/conductor/pull/372)). An `output:` 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 same `ValidationError` a 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:
`pattern` on a number, an `enum` whose members do not match the declared type,
`minLength` above `maxLength`, a regex that does not compile. Unknown keys are
rejected as well, so a misspelled `minlength` fails validation instead of
quietly leaving the field unconstrained. `required: false` is allowed only
inside object properties — a root-level output field cannot be optional.

Two things worth knowing when using them. `pattern` runs under a one-second
deadline on a `re`-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 with `StrictUndefined`, so a `nullable` field that came
back null renders as `None` and an omitted optional property raises — guard
both with `is not none` / `is defined`, as
[`examples/output-constraints.yaml`](examples/output-constraints.yaml) shows.
See [`docs/workflow-syntax.md`](docs/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: its `SKILL.md` routinely
Expand Down
71 changes: 70 additions & 1 deletion docs/workflow-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,16 @@ agents:

output: # Optional: Output schema for validation
field_name:
type: string
type: string # string | number | boolean | array | object
description: "Field purpose"
enum: ["a", "b"] # Optional: Allowed scalar values (string/number/boolean)
pattern: "^[a-z]+$" # Optional: Regex pattern (string type only)
minimum: 0 # Optional: Inclusive minimum (number type only)
maximum: 100 # Optional: Inclusive maximum (number type only)
minLength: 1 # Optional: Minimum string length (string type only)
maxLength: 50 # Optional: Maximum string length (string type only)
nullable: true # Optional: Allow null value (default: false)
required: false # Optional: Only inside object properties (default: true)

output_mode: raw # Optional: raw | envelope (default: inferred)
# raw: skip JSON extraction, wrap response
Expand Down Expand Up @@ -206,6 +214,67 @@ A response that parses as JSON but isn't an object at all (a bare `42` or an arr
This is useful when you know an agent's output is simple and a single attempt should suffice, or when you want to fail fast instead of burning tokens on recovery loops.

When the budget runs out, a schema-shape failure raises the specific validation error naming the offending field and its expected type, while a syntax failure raises a provider error. Each recovery attempt emits an `agent_parse_recovery` event, visible in the dashboard activity stream and the structured event log.

### Field Constraints

Output field definitions support optional validation constraints to enforce value boundaries and formatting rules.

| Field | Applicable Type | Description | Semantics |
|-------|-----------------|-------------|-----------|
| `enum` | `string`, `number`, `boolean` | List of allowed scalar values | Uses exact value comparison. Cannot contain `null` (use `nullable: true` instead). |
| `pattern` | `string` | Regular expression pattern | Python `re.search` matching (unanchored by default; use `^` and `$` to anchor). Evaluated consistently on all providers. Matching is time-bounded (1 second); a pathological pattern fails validation instead of hanging the run. |
| `minimum` | `number` | Inclusive minimum numeric bound | Value must be greater than or equal to `minimum`. |
| `maximum` | `number` | Inclusive maximum numeric bound | Value must be less than or equal to `maximum`. |
| `minLength` | `string` | Inclusive minimum string length | String length must be greater than or equal to `minLength`. |
| `maxLength` | `string` | Inclusive maximum string length | String length must be less than or equal to `maxLength`. |
| `required` | Any (object property only) | Whether the object property must be present | Default: `true`. **Must be `true` for root-level fields**; setting `required: false` at the root level is rejected by `conductor validate`. |
| `nullable` | Any | Whether `null` is an acceptable value | Default: `false`. When `true`, renders as `type: [T, "null"]` in JSON Schema. |

#### JSON Schema and Validation Semantics

- **Inclusive Bounds**: `minimum`, `maximum`, `minLength`, and `maxLength` represent inclusive bounds.
- **Regex Pattern Matching**: `pattern` uses Python `re.search` semantics across all providers (including Claude). It matches anywhere in the target string unless explicitly anchored with `^` and `$`. Matching runs on a `re`-compatible engine with a 1-second wall-clock deadline per check: model output is untrusted input, so a pattern with catastrophic backtracking raises a validation error (which drives the provider's output-recovery loop) instead of stalling the workflow.
- **Nullable Fields**: Setting `nullable: true` renders the JSON Schema type as `type: [T, "null"]`, allowing the field to hold `null` or a value matching `type`.
- **Optional Object Properties**: The `required: false` constraint is permitted **only inside nested object properties** (e.g. `properties.details.required: false`). All root-level output fields must be required, so setting `required: false` on a root-level agent output field will be rejected during workflow validation (`conductor validate`).

#### Field Constraints Example

```yaml
agents:
- name: evaluator
prompt: "Evaluate the artifact and return structured metrics."
output:
status:
type: string
enum: ["passed", "failed", "pending"]
description: "Execution status"
score:
type: number
minimum: 0
maximum: 100
description: "Evaluation score between 0 and 100"
code:
type: string
pattern: "^ERR-[0-9]{3}$"
minLength: 7
maxLength: 7
description: "Error code in format ERR-123"
notes:
type: string
nullable: true
description: "Optional notes or null when absent"
metadata:
type: object
description: "Additional execution metadata"
properties:
reviewer:
type: string
description: "Reviewer identifier"
comments:
type: string
required: false
description: "Optional comments property inside object"
```
### Choosing whether to declare `output:`

Declaring `output:` does two things at once: it asks the model to return JSON matching the schema, and it parses the response as structured JSON. For some agents that's what you want. For others it produces parse-recovery loops and burns tokens.
Expand Down
88 changes: 88 additions & 0 deletions examples/output-constraints.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Output Field Constraints Workflow
#
# Demonstrates structured output schema constraints:
# - `enum`: allowed scalar values
# - `pattern`: regular expression matching (Python re.search semantics)
# - `minimum` and `maximum`: numeric range boundaries
# - `minLength` and `maxLength`: string length boundaries
# - `required`: optional object properties (`required: false` inside object)
# - `nullable`: fields that permit null values (`nullable: true`)
#
# Usage:
# conductor run examples/output-constraints.yaml
# conductor validate examples/output-constraints.yaml

workflow:
name: output-constraints
description: Workflow demonstrating all eight output field schema constraints
version: "1.0.0"
entry_point: audit_evaluator

runtime:
provider: copilot

input:
ticket_id:
type: string
required: false
default: "TICK-1234"
description: Ticket identifier to evaluate

agents:
- name: audit_evaluator
description: Evaluates a system audit ticket and returns structured metrics with schema constraints
model: gpt-5.5
prompt: |
Evaluate the audit ticket {{ workflow.input.ticket_id }}.
Return the verdict, score, reference ticket, summary, optional notes, and metadata object.
output:
verdict:
type: string
enum: ["passed", "failed", "inconclusive"]
description: Audit verdict (enum constraint)
ticket_ref:
type: string
pattern: "^TICK-[0-9]{4}$"
description: Formatted ticket reference matching pattern TICK-1234
score:
type: number
minimum: 0.0
maximum: 100.0
description: Numeric evaluation score between 0 and 100
summary:
type: string
minLength: 5
maxLength: 200
description: Brief summary string between 5 and 200 characters
notes:
type: string
nullable: true
description: Optional notes string or null when omitted
details:
type: object
description: Execution details object with an optional property
properties:
reviewer:
type: string
description: Identifier of the reviewer
comments:
type: string
required: false
description: Optional comments property inside object

routes:
- to: $end

# Templates render with StrictUndefined, so both of the fields below need a
# guard: `notes` is nullable (an unguarded null renders the string "None"), and
# `details.comments` is `required: false` (referencing it raises a template
# error when the model omits it).
output:
verdict: "{{ audit_evaluator.output.verdict }}"
ticket_ref: "{{ audit_evaluator.output.ticket_ref }}"
score: "{{ audit_evaluator.output.score }}"
summary: "{{ audit_evaluator.output.summary }}"
notes: "{{ audit_evaluator.output.notes if audit_evaluator.output.notes is not none else '' }}"
comments: >-
{{ audit_evaluator.output.details.comments
if audit_evaluator.output.details.comments is defined else '' }}
41 changes: 41 additions & 0 deletions plugins/conductor/skills/conductor/references/yaml-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ agents:
<field_name>:
type: string # "string", "number", "boolean", "array", "object"
description: string # Field description
enum: [a, b] # Allowed values (string/number/boolean only)
pattern: string # Regex, string type only (Python re.search)
minimum: number # Inclusive lower bound, number type only
maximum: number # Inclusive upper bound, number type only
minLength: int # Inclusive min length, string type only
maxLength: int # Inclusive max length, string type only
nullable: bool # Allow null (default false)
required: bool # Default true; false only inside properties
items: # For array types: schema of items
type: string
properties: # For object types: schema of properties
Expand Down Expand Up @@ -606,6 +614,39 @@ output:
description: Item count
```

### Field Constraints

Optional keywords that turn a declared type into an enforced contract. They are
advertised to the model in the generated schema and validated after the response
comes back, so a violation drives the provider's output-recovery retry.
Constraints apply recursively inside object properties and array items.

| Keyword | Applies to | Meaning |
|---------|-----------|---------|
| `enum` | `string`, `number`, `boolean` | Allowed values. Cannot contain null — use `nullable: true`. |
| `pattern` | `string` | Regex, Python `re.search` semantics on every provider. Unanchored unless you write `^`/`$`. Bounded to 1 second, so a backtracking pattern fails validation instead of hanging the run. |
| `minimum` / `maximum` | `number` | Inclusive numeric bounds. |
| `minLength` / `maxLength` | `string` | Inclusive length bounds. |
| `nullable` | any | Permit null. Default `false`. |
| `required` | object properties | Default `true`. Rejected on a root-level output field — valid only inside `properties`. |

Illegal combinations fail at load time: `pattern` on a number, an `enum` member
that does not match the declared type, `minLength` above `maxLength`, a regex
that does not compile. Unknown keys are rejected too, so a misspelled
`minlength` fails validation rather than silently leaving the field
unconstrained.

**Referencing optional and nullable fields in templates.** Templates render with
`StrictUndefined`, so referencing an omitted `required: false` property raises a
template error, and a `nullable` field that came back null renders the string
`None`. Guard both:

```yaml
output:
notes: "{{ agent.output.notes if agent.output.notes is not none else '' }}"
comments: "{{ agent.output.details.comments if agent.output.details.comments is defined else '' }}"
```

## Template Syntax

### Variable Access
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ dependencies = [
"websockets>=12.0",
"httpx>=0.27.0",
"packaging>=21.0",
# Timeout-bounded regex matching for output field constraints and provider
# response normalization. Ships typed stubs and supports per-match deadlines.
"regex>=2024.11.6",
]

[project.optional-dependencies]
Expand Down
Loading
Loading