Skip to content

Commit 0793a8c

Browse files
authored
feat: add container shell + skills support (#2469)
1 parent f76ec8a commit 0793a8c

18 files changed

+1192
-46
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import argparse
2+
import asyncio
3+
import base64
4+
from pathlib import Path
5+
from tempfile import TemporaryDirectory
6+
from zipfile import ZIP_DEFLATED, ZipFile
7+
8+
from openai.types.responses import ResponseFunctionShellToolCall
9+
from openai.types.responses.response_container_reference import ResponseContainerReference
10+
11+
from agents import Agent, Runner, ShellTool, ShellToolInlineSkill, trace
12+
from agents.items import ModelResponse
13+
14+
SKILL_NAME = "csv-workbench"
15+
SKILL_DIR = Path(__file__).resolve().parent / "skills" / SKILL_NAME
16+
17+
18+
def build_skill_zip_bundle() -> bytes:
19+
with TemporaryDirectory(prefix="agents-inline-skill-") as temp_dir:
20+
zip_path = Path(temp_dir) / f"{SKILL_NAME}.zip"
21+
with ZipFile(zip_path, "w", compression=ZIP_DEFLATED) as archive:
22+
for path in sorted(SKILL_DIR.rglob("*")):
23+
if path.is_file():
24+
archive.write(path, f"{SKILL_NAME}/{path.relative_to(SKILL_DIR)}")
25+
return zip_path.read_bytes()
26+
27+
28+
def build_inline_skill() -> ShellToolInlineSkill:
29+
bundle = build_skill_zip_bundle()
30+
return {
31+
"type": "inline",
32+
"name": SKILL_NAME,
33+
"description": "Analyze CSV files in /mnt/data and return concise numeric summaries.",
34+
"source": {
35+
"type": "base64",
36+
"media_type": "application/zip",
37+
"data": base64.b64encode(bundle).decode("ascii"),
38+
},
39+
}
40+
41+
42+
def extract_container_id(raw_responses: list[ModelResponse]) -> str | None:
43+
for response in raw_responses:
44+
for item in response.output:
45+
if isinstance(item, ResponseFunctionShellToolCall) and isinstance(
46+
item.environment, ResponseContainerReference
47+
):
48+
return item.environment.container_id
49+
50+
return None
51+
52+
53+
async def main(model: str) -> None:
54+
inline_skill = build_inline_skill()
55+
56+
with trace("container_shell_inline_skill_example"):
57+
agent1 = Agent(
58+
name="Container Shell Agent (Inline Skill)",
59+
model=model,
60+
instructions="Use the available container skill to answer user requests.",
61+
tools=[
62+
ShellTool(
63+
environment={
64+
"type": "container_auto",
65+
"network_policy": {"type": "disabled"},
66+
"skills": [inline_skill],
67+
}
68+
)
69+
],
70+
)
71+
72+
result1 = await Runner.run(
73+
agent1,
74+
(
75+
"Use the csv-workbench skill. Create /mnt/data/orders.csv with columns "
76+
"id,region,amount,status and at least 6 rows. Then report total amount by "
77+
"region and count failed orders."
78+
),
79+
)
80+
print(f"Agent: {result1.final_output}")
81+
82+
container_id = extract_container_id(result1.raw_responses)
83+
if not container_id:
84+
raise RuntimeError("Container ID was not returned in shell call output.")
85+
86+
print(f"[info] Reusing container_id={container_id}")
87+
88+
agent2 = Agent(
89+
name="Container Reference Shell Agent",
90+
model=model,
91+
instructions="Reuse the existing shell container and answer concisely.",
92+
tools=[
93+
ShellTool(
94+
environment={
95+
"type": "container_reference",
96+
"container_id": container_id,
97+
}
98+
)
99+
],
100+
)
101+
102+
result2 = await Runner.run(
103+
agent2,
104+
"Run `ls -la /mnt/data`, then summarize in one sentence.",
105+
)
106+
print(f"Agent (container reuse): {result2.final_output}")
107+
108+
109+
if __name__ == "__main__":
110+
parser = argparse.ArgumentParser()
111+
parser.add_argument(
112+
"--model",
113+
default="gpt-5.2",
114+
help="Model name to use.",
115+
)
116+
args = parser.parse_args()
117+
asyncio.run(main(args.model))
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import argparse
2+
import asyncio
3+
import os
4+
5+
from openai.types.responses import ResponseFunctionShellToolCall
6+
from openai.types.responses.response_container_reference import ResponseContainerReference
7+
8+
from agents import Agent, Runner, ShellTool, ShellToolSkillReference, trace
9+
from agents.items import ModelResponse
10+
11+
SHELL_SKILL_ID_ENV = "OPENAI_SHELL_SKILL_ID"
12+
SHELL_SKILL_VERSION_ENV = "OPENAI_SHELL_SKILL_VERSION"
13+
DEFAULT_SKILL_REFERENCE: ShellToolSkillReference = {
14+
"type": "skill_reference",
15+
"skill_id": "skill_698bbe879adc81918725cbc69dcae7960bc5613dadaed377",
16+
"version": "1",
17+
}
18+
19+
20+
def resolve_skill_reference() -> ShellToolSkillReference:
21+
skill_id = os.environ.get(SHELL_SKILL_ID_ENV)
22+
if not skill_id:
23+
return DEFAULT_SKILL_REFERENCE
24+
25+
reference: ShellToolSkillReference = {"type": "skill_reference", "skill_id": skill_id}
26+
skill_version = os.environ.get(SHELL_SKILL_VERSION_ENV)
27+
if skill_version:
28+
reference["version"] = skill_version
29+
return reference
30+
31+
32+
def extract_container_id(raw_responses: list[ModelResponse]) -> str | None:
33+
for response in raw_responses:
34+
for item in response.output:
35+
if isinstance(item, ResponseFunctionShellToolCall) and isinstance(
36+
item.environment, ResponseContainerReference
37+
):
38+
return item.environment.container_id
39+
40+
return None
41+
42+
43+
async def main(model: str) -> None:
44+
skill_reference = resolve_skill_reference()
45+
print(
46+
"[info] Using skill reference:",
47+
skill_reference["skill_id"],
48+
f"(version {skill_reference.get('version', 'default')})",
49+
)
50+
51+
with trace("container_shell_skill_reference_example"):
52+
agent1 = Agent(
53+
name="Container Shell Agent (Skill Reference)",
54+
model=model,
55+
instructions="Use the available container skill to answer user requests.",
56+
tools=[
57+
ShellTool(
58+
environment={
59+
"type": "container_auto",
60+
"network_policy": {"type": "disabled"},
61+
"skills": [skill_reference],
62+
}
63+
)
64+
],
65+
)
66+
67+
result1 = await Runner.run(
68+
agent1,
69+
(
70+
"Use the csv-workbench skill. Create /mnt/data/orders.csv with columns "
71+
"id,region,amount,status and at least 6 rows. Then report total amount by "
72+
"region and count failed orders."
73+
),
74+
)
75+
print(f"Agent: {result1.final_output}")
76+
77+
container_id = extract_container_id(result1.raw_responses)
78+
if not container_id:
79+
raise RuntimeError("Container ID was not returned in shell call output.")
80+
81+
print(f"[info] Reusing container_id={container_id}")
82+
83+
agent2 = Agent(
84+
name="Container Reference Shell Agent",
85+
model=model,
86+
instructions="Reuse the existing shell container and answer concisely.",
87+
tools=[
88+
ShellTool(
89+
environment={
90+
"type": "container_reference",
91+
"container_id": container_id,
92+
}
93+
)
94+
],
95+
)
96+
97+
result2 = await Runner.run(
98+
agent2,
99+
"Run `ls -la /mnt/data`, then summarize in one sentence.",
100+
)
101+
print(f"Agent (container reuse): {result2.final_output}")
102+
103+
104+
if __name__ == "__main__":
105+
parser = argparse.ArgumentParser()
106+
parser.add_argument(
107+
"--model",
108+
default="gpt-5.2",
109+
help="Model name to use.",
110+
)
111+
args = parser.parse_args()
112+
asyncio.run(main(args.model))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
name: csv-workbench
3+
description: Analyze CSV files in /mnt/data and return concise numeric summaries.
4+
---
5+
6+
# CSV Workbench
7+
8+
Use this skill when the user asks for quick analysis of tabular data.
9+
10+
## Workflow
11+
12+
1. Inspect the CSV schema first (`head`, `python csv.DictReader`, or both).
13+
2. Compute requested aggregates with a short Python script.
14+
3. Return concise results with concrete numbers and units when available.
15+
16+
## Constraints
17+
18+
- Prefer Python stdlib for portability.
19+
- If data is missing or malformed, state assumptions clearly.
20+
- Keep the final answer short and actionable.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# CSV Playbook
2+
3+
## Quick checks
4+
5+
- Preview rows: `head -n 10 /mnt/data/your-file.csv`.
6+
- Count rows:
7+
8+
```bash
9+
python - <<'PY'
10+
import csv
11+
12+
with open('/mnt/data/your-file.csv', newline='') as f:
13+
print(sum(1 for _ in csv.DictReader(f)))
14+
PY
15+
```
16+
17+
## Grouped totals template
18+
19+
```bash
20+
python - <<'PY'
21+
import csv
22+
from collections import defaultdict
23+
24+
totals = defaultdict(float)
25+
with open('/mnt/data/your-file.csv', newline='') as f:
26+
for row in csv.DictReader(f):
27+
totals[row['region']] += float(row['amount'])
28+
29+
for region in sorted(totals):
30+
print(region, round(totals[region], 2))
31+
PY
32+
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires-python = ">=3.9"
77
license = "MIT"
88
authors = [{ name = "OpenAI", email = "support@openai.com" }]
99
dependencies = [
10-
"openai>=2.9.0,<3",
10+
"openai>=2.19.0,<3",
1111
"pydantic>=2.12.3, <3",
1212
"griffe>=1.5.6, <2",
1313
"typing-extensions>=4.12.2, <5",

src/agents/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,20 @@
125125
ShellExecutor,
126126
ShellResult,
127127
ShellTool,
128+
ShellToolContainerAutoEnvironment,
129+
ShellToolContainerNetworkPolicy,
130+
ShellToolContainerNetworkPolicyAllowlist,
131+
ShellToolContainerNetworkPolicyDisabled,
132+
ShellToolContainerNetworkPolicyDomainSecret,
133+
ShellToolContainerReferenceEnvironment,
134+
ShellToolContainerSkill,
135+
ShellToolEnvironment,
136+
ShellToolHostedEnvironment,
137+
ShellToolInlineSkill,
138+
ShellToolInlineSkillSource,
139+
ShellToolLocalEnvironment,
140+
ShellToolLocalSkill,
141+
ShellToolSkillReference,
128142
Tool,
129143
ToolOutputFileContent,
130144
ToolOutputFileContentDict,
@@ -351,6 +365,20 @@ def enable_verbose_stdout_logging():
351365
"ShellCallOutcome",
352366
"ShellCommandOutput",
353367
"ShellCommandRequest",
368+
"ShellToolLocalSkill",
369+
"ShellToolSkillReference",
370+
"ShellToolInlineSkillSource",
371+
"ShellToolInlineSkill",
372+
"ShellToolContainerSkill",
373+
"ShellToolContainerNetworkPolicyDomainSecret",
374+
"ShellToolContainerNetworkPolicyAllowlist",
375+
"ShellToolContainerNetworkPolicyDisabled",
376+
"ShellToolContainerNetworkPolicy",
377+
"ShellToolLocalEnvironment",
378+
"ShellToolContainerAutoEnvironment",
379+
"ShellToolContainerReferenceEnvironment",
380+
"ShellToolHostedEnvironment",
381+
"ShellToolEnvironment",
354382
"ShellExecutor",
355383
"ShellResult",
356384
"ShellTool",

src/agents/items.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Response,
1212
ResponseComputerToolCall,
1313
ResponseFileSearchToolCall,
14+
ResponseFunctionShellToolCallOutput,
1415
ResponseFunctionToolCall,
1516
ResponseFunctionWebSearch,
1617
ResponseInputItemParam,
@@ -253,6 +254,7 @@ class ToolCallItem(RunItemBase[Any]):
253254
FunctionCallOutput,
254255
ComputerCallOutput,
255256
LocalShellCallOutput,
257+
ResponseFunctionShellToolCallOutput,
256258
dict[str, Any],
257259
]
258260

0 commit comments

Comments
 (0)