Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 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
70 changes: 70 additions & 0 deletions docs/user_guides/synth.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,76 @@ 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 the execution model.

- **`stateful` environments** maintain shared JSON state. Tool calls read from or update that state, which is how consistency is preserved across turns.
- **`stateless` environments** generate tool results with an LLM. Responses are cached by input, so the same tool input can reuse the same generated output.
- **`deterministic` environments** behave like lookup tables. Matching inputs return responses from a predefined set without LLM generation.

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.
- `generated_output` is only used for tools in `stateless` environments.
- `deterministic_outputs` is only used for tools in `deterministic` environments.
- `read_only` is only meaningful for tools in `stateful` environments.

Example:

```yaml
environment_config:
environments:
- id: support_backend
name: Support Backend
description: Simulated support system state
type: stateful
system_prompt: You manage support system state.
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

- id: faq_lookup
name: FAQ Lookup
description: Cached LLM-backed FAQ answers
type: stateless
system_prompt: Generate concise FAQ answers grounded in the tool contract.
tools:
- id: answer_faq
name: AnswerFAQ
description: Answer common support questions.
generated_output:
instruction: Return the FAQ answer for the given question.

- 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.
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_tools: [get_ticket, answer_faq, get_refund_policy]
Comment thread
aniruddh-alt marked this conversation as resolved.
Outdated
```

## Complete Configuration Reference

### Top-Level Parameters
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ dependencies = [
"aioresponses>=0.7,<0.8", # User by inference engine tests
"backoff>=2.2.1,<2.3",
"click<8.4.0", # Used by CLI. 8.2.0 is currently unsupported by Typer.
"datasets>=3.2,<4.8.5",
"datasets>=3.2,<5",
"greenlet", # Required by skypilot 0.11+ (sqlalchemy asyncio)
"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 Expand Up @@ -304,7 +305,6 @@ unsupported-operator = "warn" # Type narrowing limitations with isinstance
too-many-positional-arguments = "warn" # Loose typing (Callable) doesn't capture signatures
parameter-already-assigned = "warn" # False positives with *args/**kwargs patterns


[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests"]
Expand Down
20 changes: 20 additions & 0 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 @@ -158,12 +159,23 @@
from oumi.core.configs.synthesis_config import SynthesisConfig
from oumi.core.configs.training_config import TrainingConfig
from oumi.core.configs.tuning_config import TuningConfig
from oumi.environments import (
BaseEnvironment,
BaseTool,
DeterministicEnvironment,
DeterministicToolOutput,
GeneratedToolOutput,
StatefulEnvironment,
StatelessEnvironment,
ToolEnvironmentType,
)

__all__ = [
"AsyncEvaluationConfig",
"AutoWrapPolicy",
"BackwardPrefetch",
"BaseConfig",
"BaseEnvironment",
"DataParams",
"DatasetParams",
"DatasetSplit",
Expand All @@ -176,6 +188,7 @@
"EvaluationBackend",
"EvaluationConfig",
"EvaluationTaskParams",
"EnvironmentConfig",
"FSDPParams",
"GenerationParams",
"GrpoParams",
Expand Down Expand Up @@ -211,17 +224,24 @@
"TuningParams",
"AttributeCombination",
"DatasetSourceParam",
"DeterministicToolOutput",
"DeterministicEnvironment",
"DocumentSegmentationParams",
"DocumentSource",
"ExampleSource",
"GeneratedToolOutput",
"GeneratedAttributePostprocessingParams",
"GeneralSynthesisParams",
"GeneratedAttribute",
"SampledAttribute",
"SampledAttributeValue",
"SegmentationStrategy",
"StatefulEnvironment",
"StatelessEnvironment",
"TextConversation",
"TextMessage",
"BaseTool",
"ToolEnvironmentType",
"TransformationStrategy",
"TransformationType",
"TransformedAttribute",
Expand Down
132 changes: 132 additions & 0 deletions src/oumi/core/configs/environment_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# 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 dataclasses import dataclass, field
from typing import Any

from oumi.core.configs.base_config import BaseConfig
from oumi.environments import BaseEnvironment, BaseTool


@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[BaseTool]:
"""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) -> BaseTool | 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[BaseTool]:
"""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."""
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