Skip to content
Closed
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
32 changes: 27 additions & 5 deletions api/oss/src/apis/fastapi/tools/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from typing import List, Optional, Union
from typing import Any, List, Optional, Union

from pydantic import BaseModel
from agenta.sdk.agents.tools import (
BuiltinToolConfig,
GatewayToolConfig,
ToolConfigurationError,
coerce_tool_configs,
)
from pydantic import BaseModel, Field, field_validator

from oss.src.core.tools.dtos import (
# Tool Catalog
Expand Down Expand Up @@ -98,10 +104,26 @@ class ToolCallResponse(BaseModel):


class ToolResolveRequest(BaseModel):
tools: List[AgentToolReference] = []
tools: List[AgentToolReference] = Field(default_factory=list)

@field_validator("tools", mode="before")
@classmethod
def _coerce_tools(cls, value: Any) -> List[AgentToolReference]:
try:
configs = coerce_tool_configs(value or []).tool_configs
except ToolConfigurationError as exc:
raise ValueError(str(exc)) from exc
unsupported = [
config
for config in configs
if not isinstance(config, (BuiltinToolConfig, GatewayToolConfig))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This coercion is what keeps /tools/resolve the gateway-only endpoint: it runs the shared coerce_tool_configs and rejects anything other than builtin or gateway, so code/client/MCP tools cannot leak into this path.

]
if unsupported:
raise ValueError("/tools/resolve accepts only builtin and gateway tools")
return configs


class ToolResolveResponse(BaseModel):
count: int = 0
builtins: List[str] = []
custom: List[ResolvedAgentTool] = []
builtins: List[str] = Field(default_factory=list)
custom: List[ResolvedAgentTool] = Field(default_factory=list)
32 changes: 7 additions & 25 deletions api/oss/src/core/tools/dtos.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
from typing import Any, Dict, List, Optional, Union

from agenta.sdk.agents.tools import BuiltinToolConfig, GatewayToolConfig
from agenta.sdk.models.workflows import JsonSchemas
from pydantic import BaseModel, Field

Expand Down Expand Up @@ -250,28 +251,9 @@ class ToolExecutionResponse(BaseModel):
# them into model-ready specs at invoke time (see ToolsService.resolve_agent_tools).


class AgentBuiltinTool(BaseModel):
"""A Pi built-in tool, referenced by name (e.g. ``read``, ``bash``)."""

type: Literal["builtin"] = "builtin"
name: str


class AgentComposioTool(BaseModel):
"""A Composio action, carrying the slug segments ``/tools/call`` parses."""

type: Literal["composio"] = "composio"
integration: str
action: str
connection: str
# Function name shown to the model. Defaults to ``{integration}__{action}``.
name: Optional[str] = None


AgentToolReference = Annotated[
Union[AgentBuiltinTool, AgentComposioTool],
Field(discriminator="type"),
]
AgentBuiltinTool = BuiltinToolConfig
AgentComposioTool = GatewayToolConfig
AgentToolReference = Union[BuiltinToolConfig, GatewayToolConfig]


class ResolvedAgentTool(BaseModel):
Expand All @@ -294,5 +276,5 @@ class AgentToolsResolution(BaseModel):
``customTools`` whose ``execute`` routes through ``/tools/call``.
"""

builtins: List[str] = []
custom: List[ResolvedAgentTool] = []
builtins: List[str] = Field(default_factory=list)
custom: List[ResolvedAgentTool] = Field(default_factory=list)
1 change: 1 addition & 0 deletions api/oss/tests/pytest/unit/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

79 changes: 79 additions & 0 deletions api/oss/tests/pytest/unit/tools/test_agent_resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

from types import SimpleNamespace
from uuid import uuid4

import pytest
from pydantic import ValidationError

from agenta.sdk.agents.tools import BuiltinToolConfig, GatewayToolConfig

from oss.src.apis.fastapi.tools.models import ToolResolveRequest
from oss.src.core.tools.dtos import AgentBuiltinTool, AgentComposioTool
from oss.src.core.tools.service import ToolsService


def test_api_reuses_sdk_tool_config_classes():
assert AgentBuiltinTool is BuiltinToolConfig
assert AgentComposioTool is GatewayToolConfig


def test_resolve_request_coerces_legacy_composio_shape():
request = ToolResolveRequest(
tools=[
"read",
{
"type": "composio",
"integration": "github",
"action": "GET_USER",
"connection": "c1",
},
]
)
assert isinstance(request.tools[0], BuiltinToolConfig)
assert isinstance(request.tools[1], GatewayToolConfig)


def test_resolve_request_rejects_non_gateway_runtime_tools():
with pytest.raises(ValidationError, match="only builtin and gateway"):
ToolResolveRequest(
tools=[
{
"type": "code",
"name": "calc",
"script": "...",
}
]
)


async def test_api_resolution_returns_stable_call_reference(monkeypatch):
service = object.__new__(ToolsService)

async def _connection(**_kwargs):
return object()

async def _action(**_kwargs):
return SimpleNamespace(
description="Get user",
schemas=SimpleNamespace(
inputs={"type": "object", "properties": {}},
),
)

monkeypatch.setattr(service, "resolve_connection_by_slug", _connection)
monkeypatch.setattr(service, "get_action", _action)

result = await service.resolve_agent_tools(
project_id=uuid4(),
tools=[
BuiltinToolConfig(name="read"),
GatewayToolConfig(
integration="github",
action="GET_USER",
connection="c1",
),
],
)
assert result.builtins == ["read"]
assert result.custom[0].call_ref == "tools.composio.github.GET_USER.c1"
33 changes: 20 additions & 13 deletions sdks/python/agenta/sdk/engines/running/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,19 +529,26 @@ def llm_inputs_schema(
schemas=dict( # type: ignore
parameters=obj(
properties={
"model": scalar(
jtype="string",
default="gpt-5.5",
description="Model the agent runs on.",
),
"agents_md": scalar(
jtype="string",
default=(
"You are a friendly hello-world agent running on the "
"Agenta agent service.\n\n- Greet the user warmly.\n- "
"Answer the user's message in one or two short sentences."
),
description="The agent's instructions (AGENTS.md).",
# One composite control for the whole agent config. The field shape lives in
# `AgentConfigSchema` (agenta.sdk.utils.types), registered as the `agent_config`
# catalog type; the playground resolves this ref and renders the AgentConfigControl.
"agent": semantic_field(
x_ag_type_ref="agent_config",
jtype="object",
description="The agent's instructions, model, tools, MCP servers, and runtime.",
default={
"agents_md": (
"You are a friendly hello-world agent running on the "
"Agenta agent service.\n\n- Greet the user warmly.\n- "
"Answer the user's message in one or two short sentences."
),
"model": "gpt-5.5",
"tools": [],
"mcp_servers": [],
"harness": "pi",
"sandbox": "local",
"permission_policy": "auto",
},
),
},
additional_properties=True,
Expand Down
80 changes: 80 additions & 0 deletions sdks/python/agenta/sdk/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pydantic import Field, model_validator, AliasChoices


from agenta.sdk.agents.mcp import MCPServerConfig
from agenta.sdk.agents.tools import ToolConfig
from agenta.sdk.utils.assets import supported_llm_models, model_metadata
from agenta.sdk.utils.helpers import _PLACEHOLDER_RE
from agenta.sdk.utils.rendering import (
Expand Down Expand Up @@ -1052,6 +1054,81 @@ def _model_catalog_type() -> dict:
}


_DEFAULT_AGENT_MODEL = "gpt-5.5"
_DEFAULT_AGENTS_MD = (
"You are a friendly hello-world agent running on the Agenta agent service.\n\n"
"- Greet the user warmly.\n"
"- Answer the user's message in one or two short sentences."
)


class AgentConfigSchema(AgSchemaMixin):
"""The playground's editable agent config (the ``agent`` element), as one semantic type.

This is the schema-generation counterpart to the runtime :class:`agenta.sdk.agents.AgentConfig`
parser: it exists only to emit a rich JSON Schema for the ``agent_config`` control, so the
field shapes live in Pydantic (single source of truth) instead of a hand-written literal.
It deliberately composes the editable fields the control surfaces — the neutral config
(``agents_md``/``model``/``tools``/``mcp_servers``) plus the run selection
(``harness``/``sandbox``/``permission_policy``) — and types ``tools``/``mcp_servers`` with the
real tool-def models so the playground gets typed editors. The runtime ``AgentConfig`` stays
permissive (``List[Any]``) because its job is to coerce the loose shapes the playground emits;
this model is strict because its job is to describe them.
"""

__ag_type__ = "agent_config"

agents_md: str = Field(
default=_DEFAULT_AGENTS_MD,
title="Instructions",
description="The agent's system prompt (its AGENTS.md).",
json_schema_extra={"x-ag-type": "textarea"},
)
model: str = Field(
default=_DEFAULT_AGENT_MODEL,
title="Model",
description="Model the agent runs on.",
json_schema_extra={"x-parameter": "grouped_choice"},
)
tools: List[ToolConfig] = Field(
default_factory=list,
title="Tools",
description=(
"Runnable tools the agent can call: harness built-ins, server-side gateway "
"actions (e.g. Composio), sandboxed code, or client-fulfilled tools."
),
)
mcp_servers: List[MCPServerConfig] = Field(
default_factory=list,
title="MCP servers",
description=(
"Declared MCP servers exposed to the agent. The backend resolves each server's "
"secret env from the vault at run time; tokens never live in the config."
),
)
harness: Literal["pi", "claude", "agenta"] = Field(
default="pi",
title="Harness",
description=(
"Coding agent to drive: pi, claude, or agenta (pi with Agenta's forced "
"skills, tools, and base instructions)."
),
)
sandbox: Literal["local", "daytona"] = Field(
default="local",
title="Sandbox",
description="Where the agent runs: local daemon or a Daytona sandbox.",
)
permission_policy: Literal["auto", "deny"] = Field(
default="auto",
title="Permission policy",
description=(
"How a permission-gating harness (e.g. Claude Code) handles tool-use prompts "
"in this headless run: auto-approve or deny."
),
)


CATALOG_TYPES = {
Message.ag_type(): _dereference_schema(Message.model_json_schema()),
Messages.ag_type(): _dereference_schema(Messages.model_json_schema()),
Expand All @@ -1065,4 +1142,7 @@ def _model_catalog_type() -> dict:
AgPermissions.ag_type(): _dereference_schema(AgPermissions.model_json_schema()),
AgResponse.ag_type(): _dereference_schema(AgResponse.model_json_schema()),
PromptTemplate.ag_type(): _dereference_schema(PromptTemplate.model_json_schema()),
AgentConfigSchema.ag_type(): _dereference_schema(
AgentConfigSchema.model_json_schema()
),
}
Loading
Loading