Skip to content

Commit 8ef7754

Browse files
danielmillerpclaude
andcommitted
Add ShellTool support to TemporalStreamingModel
openai-agents introduced a next-generation ShellTool (replacing LocalShellTool) that carries an environment config like {"type": "local", "skills": [...]}. The Temporal streaming model was dropping it with "Unknown tool type: ShellTool, skipping", so agents running through AgentEx/Temporal lost the tool entirely even though plain Runner.run(...) worked. Serialize ShellTool to the Responses API "shell" payload, defaulting environment to {"type": "local"} when unset. Import is guarded so users on older openai-agents versions (ShellTool not yet exported) continue to work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ced40bb commit 8ef7754

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

src/agentex/lib/core/temporal/plugins/openai_agents/models/temporal_streaming_model.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
CodeInterpreterTool,
2828
ImageGenerationTool,
2929
)
30+
31+
try:
32+
from agents.tool import ShellTool # type: ignore[attr-defined]
33+
except ImportError:
34+
ShellTool = None # type: ignore[assignment,misc]
3035
from agents.usage import Usage, InputTokensDetails, OutputTokensDetails # type: ignore[attr-defined]
3136
from agents.model_settings import MCPToolChoice
3237
from openai.types.responses import (
@@ -326,6 +331,13 @@ def _convert_tools(self, tools: list[Tool], handoffs: list[Handoff]) -> tuple[Li
326331
"type": "local_shell",
327332
})
328333

334+
elif ShellTool is not None and isinstance(tool, ShellTool):
335+
environment = dict(tool.environment) if tool.environment else {"type": "local"}
336+
response_tools.append({
337+
"type": "shell",
338+
"environment": environment,
339+
})
340+
329341
else:
330342
logger.warning(f"Unknown tool type: {type(tool).__name__}, skipping")
331343

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Unit tests for TemporalStreamingModel._convert_tools tool serialization."""
2+
3+
from unittest.mock import MagicMock, patch
4+
5+
import pytest
6+
7+
from agentex.lib.core.temporal.plugins.openai_agents.models import (
8+
temporal_streaming_model as tsm_module,
9+
)
10+
from agentex.lib.core.temporal.plugins.openai_agents.models.temporal_streaming_model import (
11+
TemporalStreamingModel,
12+
)
13+
14+
15+
@pytest.fixture
16+
def model():
17+
with patch(
18+
"agentex.lib.core.temporal.plugins.openai_agents.models.temporal_streaming_model.create_async_agentex_client"
19+
):
20+
return TemporalStreamingModel(model_name="gpt-4o", openai_client=MagicMock())
21+
22+
23+
class _FakeShellTool:
24+
"""Stand-in for agents.tool.ShellTool for environments where it isn't installed."""
25+
26+
def __init__(self, environment):
27+
self.environment = environment
28+
29+
30+
def test_shell_tool_local_environment(model, monkeypatch):
31+
"""ShellTool with a local environment should serialize to a 'shell' payload."""
32+
monkeypatch.setattr(tsm_module, "ShellTool", _FakeShellTool)
33+
34+
tool = _FakeShellTool(environment={"type": "local", "skills": ["git"]})
35+
response_tools, _ = model._convert_tools([tool], handoffs=[])
36+
37+
assert response_tools == [{"type": "shell", "environment": {"type": "local", "skills": ["git"]}}]
38+
39+
40+
def test_shell_tool_defaults_environment_when_missing(model, monkeypatch):
41+
"""ShellTool with environment=None should fall back to {'type': 'local'}."""
42+
monkeypatch.setattr(tsm_module, "ShellTool", _FakeShellTool)
43+
44+
tool = _FakeShellTool(environment=None)
45+
response_tools, _ = model._convert_tools([tool], handoffs=[])
46+
47+
assert response_tools == [{"type": "shell", "environment": {"type": "local"}}]
48+
49+
50+
def test_shell_tool_unavailable_falls_through(model, monkeypatch, caplog):
51+
"""If ShellTool isn't installed, an unknown tool should log a warning and be skipped."""
52+
monkeypatch.setattr(tsm_module, "ShellTool", None)
53+
54+
class _NotAShellTool:
55+
pass
56+
57+
with caplog.at_level("WARNING"):
58+
response_tools, _ = model._convert_tools([_NotAShellTool()], handoffs=[])
59+
60+
assert response_tools == []
61+
assert any("Unknown tool type" in rec.message for rec in caplog.records)

0 commit comments

Comments
 (0)