Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

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

### Added

- **Context tier** — new `context_tier` knob (`default` | `long_context`) to
select a model's long-context (e.g. 1M-token) window on the Copilot provider.
Set per agent via `context_tier:` (sibling to `model`) or workflow-wide via
`runtime.default_context_tier`; the per-agent value wins. It composes
independently with `reasoning.effort` (the two map to separate
`create_session` kwargs). The Copilot provider forwards the resolved value as
`context_tier` to `create_session`; other providers ignore it. Only valid on
standard `agent`-type agents (rejected on `script`, `human_gate`, and
`workflow` agents). See [`examples/context-tier.yaml`](examples/context-tier.yaml)
and [Context Tier](docs/configuration.md#context-tier).
([#251](https://github.com/microsoft/conductor/issues/251))

### Fixed

- `human_gate` agents: the dict returned by `prompt_for` text-collection fields
Expand Down
52 changes: 52 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ workflow:
temperature: 0.7
max_tokens: 4096
default_reasoning_effort: medium # low | medium | high | xhigh (optional)
default_context_tier: default # default | long_context (optional, Copilot only)
# Provider-specific settings...
```

Expand All @@ -25,6 +26,11 @@ unless it declares its own `reasoning.effort` override. See
[Reasoning Effort](#reasoning-effort) for the per-provider translation and
constraints.

The `default_context_tier` field sets a workflow-wide default for the model's
context-window tier that every provider-backed agent inherits unless it
declares its own `context_tier` override. See [Context Tier](#context-tier)
for details. This is a Copilot-only capability.

## Provider Selection

### Copilot Provider
Expand Down Expand Up @@ -323,6 +329,52 @@ Reasoning / thinking content emitted by the model is surfaced via
`agent_reasoning` events and rendered in the dashboard, JSONL logs, and
`-vv` console output for both providers.

## Context Tier

Some models expose a larger context window (e.g. a 1M-token tier) selected via
a separate session parameter rather than the model name. Conductor surfaces
this as a unified `context_tier` knob. Allowed values: `default`,
`long_context`.

Use `long_context` for heavy-reasoning agents that ingest large evidence
(multi-MB logs, many candidate source files) and would otherwise truncate at
the default (~200K) tier.

`context_tier` composes independently with `reasoning.effort` — they map to two
separate `create_session` kwargs, so an agent may set both.

Set a workflow-wide default and/or override per agent:

```yaml
workflow:
runtime:
provider: copilot
default_context_tier: default # workflow-wide default

agents:
- name: triage
# No context_tier — inherits `default` from the runtime default.
prompt: "Triage {{ workflow.input.topic }}"

- name: analyze
context_tier: long_context # per-agent override wins
reasoning:
effort: high # composes with context_tier
prompt: "Deeply analyze {{ workflow.input.topic }}"
```

Per-agent overrides always win over the workflow-wide default. The
`context_tier` field is **only** valid on standard `agent`-type agents; it is
rejected on `script`, `human_gate`, and `workflow` agents (none of which call a
model).

### Per-provider translation

- **Copilot** — Forwards the chosen tier as `context_tier` to
`CopilotClient.create_session`. No static capability validation is performed;
the SDK accepts or rejects the value at session creation.
- **Other providers** — The value is ignored; there is no equivalent knob.

## MCP Servers

Configure [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers for tool access. Both the Copilot and Claude providers support MCP tools.
Expand Down
14 changes: 14 additions & 0 deletions docs/workflow-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ workflow:
# every provider-backed agent unless it
# declares its own `reasoning.effort`.
# See docs/configuration.md#reasoning-effort.
default_context_tier: default # Optional: default | long_context (Copilot only)
# Workflow-wide default for the model's
# context-window tier. Inherited by every
# provider-backed agent unless it declares
# its own `context_tier`.
# See docs/configuration.md#context-tier.
```

**Workflow metadata** is included verbatim in the `workflow_started` event and lets downstream consumers (dashboards, queue runners, observability tools) adapt without parsing the YAML. CLI `--metadata key=value` flags merge on top of YAML metadata (CLI wins on conflicts).
Expand Down Expand Up @@ -104,6 +110,14 @@ agents:
# script, human_gate, workflow).
# See docs/configuration.md#reasoning-effort.

context_tier: long_context # Optional: per-agent context-tier override
# default | long_context (Copilot only)
# Overrides runtime.default_context_tier.
# Composes with reasoning. Only valid on
# type=agent (rejected on script,
# human_gate, workflow).
# See docs/configuration.md#context-tier.

routes: # Optional: Routing logic
- to: next_agent # Agent name or $end
when: "{{ condition }}" # Optional: Route condition
Expand Down
17 changes: 17 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ conductor run examples/reasoning-effort.yaml \

See [Reasoning Effort](../docs/configuration.md#reasoning-effort) for the per-provider translation, supported models, and validation rules.

## Context Tier

### context-tier.yaml

A two-stage workflow that demonstrates selecting a model's long-context (e.g. 1M-token) window. Demonstrates:
- Workflow-wide default via `runtime.default_context_tier`
- Per-agent override via `context_tier` (wins over the default)
- Composition with `reasoning.effort` — the two map to separate `create_session` kwargs
- Conditional routing on a structured boolean output

```bash
conductor run examples/context-tier.yaml \
--input topic="root-causing a multi-service latency regression"
```

See [Context Tier](../docs/configuration.md#context-tier) for details. This is a Copilot-only capability.

## Multi-Agent Workflows

### research-assistant.yaml
Expand Down
105 changes: 105 additions & 0 deletions examples/context-tier.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Context Tier Workflow
#
# This example demonstrates selecting a model's long-context (e.g. 1M-token)
# window for heavy-reasoning agents. It shows:
# - A workflow-wide default via `runtime.default_context_tier`
# - A per-agent override via `context_tier` (wins over the default)
# - Composition with `reasoning.effort` — the two are independent and
# map to two separate `create_session` kwargs
#
# Context tier is a Copilot-provider capability:
# - Copilot SDK: passes `context_tier` ("default" | "long_context") to
# `create_session` to select the model's context-window tier.
# - Other providers ignore the value.
#
# Use `long_context` for agents that ingest large evidence (multi-MB logs,
# many candidate source files) and would otherwise truncate at the default
# (~200K) tier.
#
# Usage:
# conductor run examples/context-tier.yaml \
# --input topic="root-causing a multi-service latency regression"

workflow:
name: context-tier
description: Demonstrates workflow-wide and per-agent context-tier configuration
version: "1.0.0"
entry_point: triage

runtime:
provider: copilot
# Workflow-wide default applied to every provider-backed agent
# unless the agent declares its own `context_tier`.
default_context_tier: default

input:
topic:
type: string
required: true
description: An investigation topic that may require ingesting large evidence

agents:
- name: triage
description: Lightweight triage that inherits the runtime default tier
model: claude-opus-4.8
# No `context_tier` here — inherits `runtime.default_context_tier: default`.
prompt: |
Briefly triage the following investigation. Decide whether a deep,
evidence-heavy analysis pass is warranted (true when it implies sifting
large logs or many source files).

Topic: {{ workflow.input.topic }}
output:
summary:
type: string
description: A short triage summary
needs_deep_analysis:
type: boolean
description: Whether a long-context analysis pass is warranted
routes:
- to: analyze
when: "{{ output.needs_deep_analysis }}"
- to: $end

- name: analyze
description: Deep analysis over large evidence (pinned to the long-context tier)
model: claude-opus-4.8
# Per-agent override: this agent always runs on the long-context tier,
# regardless of the workflow-wide default. Composes with reasoning.
context_tier: long_context
reasoning:
effort: high
input:
- workflow.input.topic
- triage.output.summary
prompt: |
Perform a thorough analysis of the investigation below. Assume you may
need to reason over a large body of evidence.

**Topic:** {{ workflow.input.topic }}

**Triage summary:**
{{ triage.output.summary }}

Produce a root-cause hypothesis, the evidence that supports it, and the
next concrete step to confirm it.
output:
hypothesis:
type: string
description: Root-cause hypothesis
evidence:
type: string
description: Evidence supporting the hypothesis
next_step:
type: string
description: The next concrete step to confirm the hypothesis
routes:
- to: $end

output:
topic: "{{ workflow.input.topic }}"
summary: "{{ triage.output.summary }}"
needs_deep_analysis: "{{ triage.output.needs_deep_analysis }}"
hypothesis: "{% if analyze is defined %}{{ analyze.output.hypothesis }}{% else %}(skipped — no deep analysis needed){% endif %}"
evidence: "{% if analyze is defined %}{{ analyze.output.evidence }}{% endif %}"
next_step: "{% if analyze is defined %}{{ analyze.output.next_step }}{% endif %}"
44 changes: 44 additions & 0 deletions src/conductor/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)

from conductor.duration import parse_duration
from conductor.providers.context_tier import ContextTier
from conductor.providers.reasoning import ReasoningEffort

# Maximum allowed wait-step duration (24 hours). Anything longer almost
Expand Down Expand Up @@ -533,6 +534,26 @@ class AgentDef(BaseModel):
Supports Jinja2 templates: {{ workflow.input.model_name }}
"""

context_tier: ContextTier | None = None
"""Context-window tier for models that support it (Copilot provider only).

Set ``context_tier: long_context`` to pin a heavy-reasoning agent to the
model's long-context (e.g. 1M-token) window. ``default`` selects the
standard tier; ``None`` sends no value (provider default).

Falls back to ``runtime.default_context_tier`` when unset. Composes
independently with ``reasoning`` — an agent may set both.

Only the Copilot provider forwards this today (maps to the SDK's
``create_session`` ``context_tier`` param). Other providers ignore it.

Only applies to provider-backed agents (type='agent' or None).

Example YAML::

context_tier: long_context
"""

input: list[str] = Field(default_factory=list)
"""Context dependencies. Format: 'agent_name.output' or 'workflow.input.param'.
Suffix with '?' for optional dependencies."""
Expand Down Expand Up @@ -892,6 +913,8 @@ def validate_agent_type(self) -> AgentDef:
raise ValueError("human_gate agents cannot have 'max_depth'")
if self.reasoning is not None:
raise ValueError("human_gate agents cannot have 'reasoning'")
if self.context_tier is not None:
raise ValueError("human_gate agents cannot have 'context_tier'")
if self.timeout_seconds is not None:
raise ValueError("human_gate agents cannot have 'timeout_seconds'")
if self.value is not None:
Expand Down Expand Up @@ -931,6 +954,8 @@ def validate_agent_type(self) -> AgentDef:
raise ValueError("script agents cannot have 'max_depth'")
if self.reasoning is not None:
raise ValueError("script agents cannot have 'reasoning'")
if self.context_tier is not None:
raise ValueError("script agents cannot have 'context_tier'")
if self.timeout_seconds is not None:
raise ValueError(
"script agents cannot have 'timeout_seconds' "
Expand Down Expand Up @@ -1016,6 +1041,8 @@ def validate_agent_type(self) -> AgentDef:
raise ValueError("wait agents cannot have 'dialog'")
if self.reasoning is not None:
raise ValueError("wait agents cannot have 'reasoning'")
if self.context_tier is not None:
raise ValueError("wait agents cannot have 'context_tier'")
if self.timeout_seconds is not None:
raise ValueError("wait agents cannot have 'timeout_seconds'")
if self.output is not None:
Expand Down Expand Up @@ -1075,6 +1102,8 @@ def validate_agent_type(self) -> AgentDef:
raise ValueError("set agents cannot have 'dialog'")
if self.reasoning is not None:
raise ValueError("set agents cannot have 'reasoning'")
if self.context_tier is not None:
raise ValueError("set agents cannot have 'context_tier'")
if self.timeout_seconds is not None:
raise ValueError("set agents cannot have 'timeout_seconds'")
if self.duration is not None:
Expand Down Expand Up @@ -1133,6 +1162,8 @@ def validate_agent_type(self) -> AgentDef:
raise ValueError("terminate agents cannot have 'dialog'")
if self.reasoning is not None:
raise ValueError("terminate agents cannot have 'reasoning'")
if self.context_tier is not None:
raise ValueError("terminate agents cannot have 'context_tier'")
if self.workflow:
raise ValueError("terminate agents cannot have 'workflow'")
if self.input_mapping is not None:
Expand Down Expand Up @@ -1183,6 +1214,8 @@ def validate_agent_type(self) -> AgentDef:
)
if self.type == "workflow" and self.reasoning is not None:
raise ValueError("workflow agents cannot have 'reasoning'")
if self.type == "workflow" and self.context_tier is not None:
raise ValueError("workflow agents cannot have 'context_tier'")
Comment thread
brrusino marked this conversation as resolved.

# Wait-only fields are forbidden on every other type. ``reason`` is
# shared with ``type: terminate`` (which has its own required-non-
Expand Down Expand Up @@ -1565,6 +1598,17 @@ def _coerce_provider(cls, value: Any) -> Any:
the request through to the SDK.
"""

default_context_tier: ContextTier | None = None
"""Workflow-wide default context-window tier (Copilot provider only).

Each agent may override with its own ``context_tier``. ``long_context``
selects a model's long-context (e.g. 1M-token) window; ``default`` selects
the standard tier; ``None`` sends no value.

Only the Copilot provider forwards this (maps to the SDK's
``create_session`` ``context_tier`` param). Other providers ignore it.
"""


class WorkflowDef(BaseModel):
"""Top-level workflow configuration."""
Expand Down
4 changes: 4 additions & 0 deletions src/conductor/engine/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ def build_workflow_started_data(self) -> dict[str, Any]:
self._system_metadata = self._build_system_metadata()

default_effort = self.config.workflow.runtime.default_reasoning_effort
default_tier = self.config.workflow.runtime.default_context_tier
default_provider_name = self.config.workflow.runtime.provider.name

# Resolve the provider per agent (honoring per-agent overrides).
Expand Down Expand Up @@ -674,6 +675,9 @@ def _record_provider(name: str) -> None:
"reasoning_effort": (
a.reasoning.effort if a.reasoning is not None else default_effort
),
"context_tier": (
Comment thread
brrusino marked this conversation as resolved.
a.context_tier if a.context_tier is not None else default_tier
),
}
for a in self.config.agents
],
Expand Down
Loading
Loading