Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
be38f5d
feat: add agentic tool synthesis skeleton
aniruddh-alt Apr 2, 2026
2df6f03
fix: add missing docstring and apply ruff format
aniruddh-alt Apr 2, 2026
6505cbc
Potential fix for pull request finding 'Unused global variable'
aniruddh-alt Apr 2, 2026
a628ea7
fix: suppress pre-existing ASYNC240/ASYNC230 ruff errors in MCP files
aniruddh-alt Apr 2, 2026
1533244
Merge remote-tracking branch 'origin/main' into aniruddh-alt/stateful…
aniruddh-alt Apr 7, 2026
1b7060b
Update tool_executor.py
aniruddh-alt Apr 7, 2026
088cd5a
refactor: move environments to top-level package with typed tool hier…
aniruddh-alt Apr 9, 2026
01ab23d
style: apply ruff format to environment and test files
aniruddh-alt Apr 9, 2026
56efd03
fix: resolve pyright type errors in environment tests
aniruddh-alt Apr 9, 2026
ea2ef62
Merge branch 'main' into aniruddh-alt/agent-environment-skeleton
aniruddh-alt Apr 9, 2026
ace8c8e
fix: update test assertion to include environment_config parameter
aniruddh-alt Apr 9, 2026
e367535
Merge branch 'main' into aniruddh-alt/agent-environment-skeleton
aniruddh-alt Apr 9, 2026
21ceb87
revert: remove unrelated MCP file changes from branch
aniruddh-alt Apr 9, 2026
ec78eb4
revert: remove unrelated datasets version bump from pyproject.toml
aniruddh-alt Apr 9, 2026
4148d55
feat: refactor environments, consolidate synthetic environments
aniruddh-alt Apr 14, 2026
13566fb
Merge remote-tracking branch 'origin/main' into aniruddh-alt/agent-en…
aniruddh-alt Apr 14, 2026
f8a4d52
Update test_tool_params.py
aniruddh-alt Apr 14, 2026
6f48a8c
refactor: move ToolResult into base_tool.py and fix circular imports
aniruddh-alt Apr 14, 2026
1a223d4
fix: resolve circular import without lazy loading
aniruddh-alt Apr 14, 2026
c5cd36d
feat: fix circular imports
aniruddh-alt Apr 14, 2026
1b46762
fix: use Any annotation for environment_config field in SynthesisConfig
aniruddh-alt Apr 14, 2026
5c9d096
Merge branch 'main' into aniruddh-alt/agent-environment-skeleton
aniruddh-alt Apr 14, 2026
4ef5f1b
Add ToolSchema class for structured tool I/O definitions
aniruddh-alt Apr 15, 2026
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
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ faq/oom

development/dev_setup
development/contributing
development/agentic_synthesis_environments
development/code_of_conduct
development/style_guide
development/docs_guide
Expand Down
98 changes: 98 additions & 0 deletions docs/user_guides/synth.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,104 @@ Ready to dive deeper? The sections below cover all available options in detail.

---

## Environment-First Tool Synthesis

Agentic synthesis now follows an environment-first model. Tools do not declare an output strategy directly. Instead, each tool is bound to an environment, and the environment type defines how tool calls are executed via its `step()` method.

- **`synthetic` environments** are backed by an LLM that simulates tool execution. They can be stateless (no persistent state) or stateful (mutable JSON state across turns). Statefulness is controlled by the optional `state_params` field — when provided, the environment tracks and mutates state across calls; when absent, each call is independent.
- **`deterministic` environments** behave like lookup tables. Each tool defines a set of input-to-output mappings, and `step()` resolves tool calls by matching arguments against those mappings. No LLM is involved.

At the config level:

- Environments own their tool definitions.
- Reusable environment catalogs live in top-level `environment_config` or `environment_config_path`.
- Tools do not declare an `environment` field. The parent environment owns the binding.
- `deterministic_outputs` is only used for tools in `deterministic` environments.
- `read_only` is only meaningful for tools in stateful `synthetic` environments.
- Multiturn attributes reference environments (not individual tools) to select which tools are available.

Example:

```yaml
environment_config:
environments:
- id: support_backend
name: Support Backend
description: Simulated support system with tickets and users
type: synthetic
system_prompt: You manage a customer support system with tickets and users.
state_params:
state_schema:
type: object
properties:
tickets: { type: array }
users: { type: array }
initial_state:
tickets: []
users: []
tools:
Comment thread
aniruddh-alt marked this conversation as resolved.
- id: get_ticket
name: GetTicket
description: Read a ticket from the support backend.
read_only: true
parameters:
type: object
properties:
ticket_id: { type: string }
- id: create_ticket
name: CreateTicket
description: Create a new support ticket.
read_only: false
parameters:
type: object
properties:
subject: { type: string }
priority: { type: string, enum: [low, medium, high] }

- id: faq_lookup
name: FAQ Lookup
description: Cached LLM-backed FAQ answers
type: synthetic
system_prompt: Generate concise FAQ answers grounded in the tool contract.
cache_by_input: true
tools:
- id: answer_faq
name: AnswerFAQ
description: Answer common support questions.
parameters:
type: object
properties:
question: { type: string }

- id: policy_table
name: Policy Table
description: Predefined policy responses
type: deterministic
tools:
- id: get_refund_policy
name: GetRefundPolicy
description: Return the matching refund policy.
parameters:
type: object
properties:
policy_type: { type: string }
deterministic_outputs:
- input:
policy_type: standard
output:
policy: Standard 30-day refund policy

strategy_params:
multiturn_attributes:
- id: support_chat
min_turns: 2
max_turns: 4
role_instruction_messages:
USER: You are a customer contacting support.
ASSISTANT: You are a helpful support agent.
available_environments: [support_backend, faq_lookup, policy_table]
```

## Complete Configuration Reference

### Top-Level Parameters
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies = [
"hdrhistogram>=0.10,<0.11",
"httpx>=0.27,<1.0", # Used by deploy module (async HTTP client)
"jsonlines",
"jsonpatch>=1.33,<2.0",
"lm_eval[wandb]>=0.4,<0.5.0",
"mlflow>=3.1", # >=3.1.4 requires Python3.10>=
"numpy>=1.26,<2.4", # verl==0.5.0 depends on numpy<2.0.0
Expand Down
10 changes: 5 additions & 5 deletions src/oumi/core/configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
)
from oumi.core.configs.async_evaluation_config import AsyncEvaluationConfig
from oumi.core.configs.base_config import BaseConfig
from oumi.core.configs.environment_config import EnvironmentConfig
from oumi.core.configs.evaluation_config import EvaluationConfig
from oumi.core.configs.inference_config import InferenceConfig
from oumi.core.configs.inference_engine_type import InferenceEngineType
Expand Down Expand Up @@ -123,7 +124,9 @@
from oumi.core.configs.params.profiler_params import ProfilerParams
from oumi.core.configs.params.remote_params import RemoteParams
from oumi.core.configs.params.synthesis_params import (
AttributeCombination,
DatasetSource as DatasetSourceParam,
)
from oumi.core.configs.params.synthesis_params import (
DocumentSegmentationParams,
DocumentSource,
ExampleSource,
Expand All @@ -140,9 +143,6 @@
TransformationType,
TransformedAttribute,
)
from oumi.core.configs.params.synthesis_params import (
DatasetSource as DatasetSourceParam,
)
from oumi.core.configs.params.telemetry_params import TelemetryParams
from oumi.core.configs.params.training_params import (
MixedPrecisionDtype,
Expand Down Expand Up @@ -175,6 +175,7 @@
"EvaluationConfig",
"EvaluationBackend",
"EvaluationConfig",
"EnvironmentConfig",
"EvaluationTaskParams",
"FSDPParams",
"GenerationParams",
Expand Down Expand Up @@ -209,7 +210,6 @@
"TunerType",
"TuningConfig",
"TuningParams",
"AttributeCombination",
"DatasetSourceParam",
"DocumentSegmentationParams",
"DocumentSource",
Expand Down
139 changes: 139 additions & 0 deletions src/oumi/core/configs/environment_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Copyright 2025 - Oumi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Configuration for agentic environments."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

from oumi.core.configs.base_config import BaseConfig

if TYPE_CHECKING:
from oumi.environments.base_environment import BaseEnvironment
from oumi.environments.base_tool import Tool


@dataclass
class EnvironmentConfig(BaseConfig):
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
aniruddh-alt marked this conversation as resolved.
Dismissed
"""Top-level config for environment-first tool definitions."""

environments: list[Any] = field(default_factory=list)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This typing is a bit too weak

"""Reusable environments and their owned tools."""

def __post_init__(self):
"""Verifies/populates params."""
self.environments = [
self._coerce_environment(environment) for environment in self.environments
]

env_ids: set[str] = set()
tool_ids: set[str] = set()

for environment in self.environments:
if environment.id in env_ids:
raise ValueError(
f"EnvironmentConfig.environments contains duplicate "
f"environment id '{environment.id}'."
)
env_ids.add(environment.id)

for tool in environment.tools:
if tool.id in tool_ids:
raise ValueError(
f"EnvironmentConfig.environments contains duplicate "
f"tool id '{tool.id}'."
)
tool_ids.add(tool.id)

@property
def all_tools(self) -> list[Tool]:
"""Flatten all tools across environments."""
return [tool for environment in self.environments for tool in environment.tools]

@property
def tool_environment_map(self) -> dict[str, str]:
"""Map each tool id to the environment that owns it."""
return {
tool.id: environment.id
for environment in self.environments
for tool in environment.tools
}

def get_environment(self, environment_id: str) -> BaseEnvironment | None:
"""Look up an environment by id."""
for environment in self.environments:
if environment.id == environment_id:
return environment
return None

def get_tool(self, tool_id: str) -> Tool | None:
"""Look up a tool by id."""
for tool in self.all_tools:
if tool.id == tool_id:
return tool
return None

def resolve_tools(
Comment thread
aniruddh-alt marked this conversation as resolved.
self,
environment_ids: list[str] | None = None,
tool_ids: list[str] | None = None,
) -> list[Tool]:
"""Resolve tools from selected environments and optional tool ids.

Raises:
ValueError: If any environment_id or tool_id is not found.
"""
all_env_ids = {env.id for env in self.environments}

if environment_ids:
unknown_envs = set(environment_ids) - all_env_ids
if unknown_envs:
raise ValueError(
f"Unknown environment id(s): {sorted(unknown_envs)}. "
f"Defined: {sorted(all_env_ids)}"
)
selected_environment_ids = environment_ids
else:
selected_environment_ids = list(all_env_ids)

selected_environments = [
environment
for environment in self.environments
if environment.id in set(selected_environment_ids)
]
tools = [
tool for environment in selected_environments for tool in environment.tools
]

if tool_ids:
available_tool_ids = {tool.id for tool in tools}
unknown_tools = set(tool_ids) - available_tool_ids
if unknown_tools:
raise ValueError(
f"Unknown tool id(s): {sorted(unknown_tools)}. "
f"Available in selected environments: "
f"{sorted(available_tool_ids)}"
)
allowed_tool_ids = set(tool_ids)
tools = [tool for tool in tools if tool.id in allowed_tool_ids]

return tools

def _coerce_environment(self, environment: Any) -> BaseEnvironment:
"""Coerce a raw dict or environment instance into a concrete environment."""
from oumi.environments.base_environment import BaseEnvironment

return BaseEnvironment.create(environment)
33 changes: 33 additions & 0 deletions src/oumi/core/configs/params/synthesis_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,16 @@ class MultiTurnAttribute:
Allows user to specify custom instructions for the planner while planning
out the conversation."""

available_environments: list[str] = field(default_factory=list)
"""List of environment ids availabe in this conversation."""

available_tools: list[str] = field(default_factory=list)
"""List of tool ids available in this conversation."""

max_tool_calls_per_turn: int = 50
"""Safety ceiling for tool calls per ASSISTANT turn. The agent naturally stops
when it decides no more tools are needed. This only prevents runaway loops."""

def __post_init__(self):
"""Verifies/populates params."""
if not self.id:
Expand Down Expand Up @@ -543,6 +553,29 @@ def __post_init__(self):
"string."
)

if self.available_tools is not None:
if not isinstance(self.available_tools, list):
raise ValueError(
"MultiTurnAttribute.available_tools must be a list of tool names."
)
for tool in self.available_tools:
if not isinstance(tool, str):
raise ValueError(
"MultiTurnAttribute.available_tools must be a list of strings."
)
if self.available_environments is not None:
if not isinstance(self.available_environments, list):
raise ValueError(
"MultiTurnAttribute.available_environments must be a list of "
"environment ids."
)
for environment in self.available_environments:
if not isinstance(environment, str):
raise ValueError(
"MultiTurnAttribute.available_environments must be a list "
"of strings."
)


class TransformationType(str, Enum):
"""Types of transformation strategies."""
Expand Down
Loading
Loading