Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
23b5e28
[skip ci] apcha/agents-files-artifacts
apcha-oai Oct 1, 2026
0ffdf87
feat(agents): prepare selected files and download turn artifacts
apcha-oai Oct 1, 2026
0beb3b4
fix(agents): retain upload sources and standard request options
apcha-oai Oct 1, 2026
6a548de
fix(agents): preserve async cleanup and offload directory selection
apcha-oai Oct 1, 2026
c6a4cea
fix(agents): preserve selected file identity and async responsiveness
apcha-oai Oct 1, 2026
6f18dc6
docs(agents): scope local path-selection assumptions
apcha-oai Oct 1, 2026
6fc7b78
feat(agents): read result artifacts in memory and keep worker cleanup…
apcha-oai Oct 1, 2026
a7cc196
test(agents): use consistent asyncio imports
apcha-oai Oct 1, 2026
baaef5d
chore(agents): order combined beta helper exports
apcha-oai Oct 1, 2026
62fcfb2
fix(agents): preserve interrupted uploads and defer file limits to API
apcha-oai Oct 1, 2026
4380393
fix(agents): validate file destination ancestors without pairwise scans
apcha-oai Oct 1, 2026
b451728
fix(agents): skip directory subtrees outside file selections
apcha-oai Oct 1, 2026
234d5c2
refactor(agents): expand file globs one directory segment at a time
apcha-oai Oct 1, 2026
94688b7
refactor(agents): use standard pathlib glob semantics for file selection
apcha-oai Oct 1, 2026
b8653b4
test(agents): consolidate directory selection scenarios
apcha-oai Oct 1, 2026
4e2b103
fix(agents): snapshot async uploads one file at a time
apcha-oai Oct 1, 2026
0aff3cc
docs(agents): note future batch upload opportunity
apcha-oai Oct 1, 2026
51dfaca
docs(agents): clarify application-owned archive orchestration
apcha-oai Oct 1, 2026
d7b99b1
Merge branch 'main' into apcha/agents-files-artifacts
apcha-oai Oct 1, 2026
e02dd44
Merge branch 'main' into apcha/agents-files-artifacts
apcha-oai Oct 1, 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
39 changes: 39 additions & 0 deletions helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -633,3 +633,42 @@ This only selects the local parser; it does not change the session's schema.
`result.parse(Report)` parses an existing raw result. `AgentOutputParseError.result`
retains the completed raw answer if validation fails. With `AsyncOpenAI`, await
creation and the result getter, and use `async with`.
### Stage files and download a turn artifact
```python
from pathlib import Path
prepared = client.beta.agents.environments.files.prepare({
"/workspace/source.pdf": Path("source.pdf"),
})
session = client.beta.agents.sessions.create(
agent={"model": MODEL},
environment={"type": "openai_hosted", "files": prepared.files},
)
with client.beta.agents.sessions.stream(
session.id, input="Read source.pdf and write /workspace/outputs/report.md."
) as stream:
result = stream.get_final_result()
artifacts = client.beta.agents.sessions.artifacts.for_result(result)
# Read in memory, or stream to an application-owned local path.
report_bytes = artifacts.content("/workspace/outputs/report.md").content
artifact = artifacts.download(
"/workspace/outputs/report.md", to=Path("downloaded-report.md")
)
```
Use `prepare_directory("docs", destination="/workspace/docs", include=["**/*.md"])`
for a selected directory snapshot, or `files.upload(environment_id, file=Path(...),
path="/workspace/source.pdf")` to stage a file in an existing environment.
Directory selection follows `Path.glob` semantics, including skipping unreadable directories.
Uploads remain caller-owned: use `prepared.uploaded_file_ids` with the ordinary
Files API when ready to delete them. Preparation errors expose partial uploads
through `error.prepared`. A batch of multiple uploads cannot share one explicit
`Idempotency-Key`.
Local path/directory uploads are intended for static application-owned files and
stable directories. They do not sandbox untrusted path selection or hostile local
filesystem writers. With `AsyncOpenAI`, await preparation, staging, and artifact downloads.
10 changes: 10 additions & 0 deletions src/openai/lib/beta/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""Beta helpers for hosted Agents API tools and turn results."""

from ._files import (
StagedAgentFile as StagedAgentFile,
PreparedAgentFiles as PreparedAgentFiles,
AgentFileStagingError as AgentFileStagingError,
AgentFilePreparationError as AgentFilePreparationError,
)
from ._tools import (
FunctionTool as FunctionTool,
function_tool as function_tool,
Expand All @@ -15,3 +21,7 @@
AgentSessionEventStream as AgentSessionEventStream,
AsyncAgentSessionEventStream as AsyncAgentSessionEventStream,
)
from ._artifacts import (
AgentResultArtifacts as AgentResultArtifacts,
AsyncAgentResultArtifacts as AsyncAgentResultArtifacts,
)
157 changes: 157 additions & 0 deletions src/openai/lib/beta/agents/_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from __future__ import annotations

from os import PathLike
from typing import TYPE_CHECKING

import httpx2

from ._files import _RequestOptions
from ._result import AgentTurnResult
from ...._types import Body, Query, Headers, NotGiven, not_given
from ...._legacy_response import HttpxBinaryResponseContent
from ....types.beta.agents.sessions.session_artifact import SessionArtifact

if TYPE_CHECKING:
from ....resources.beta.agents.sessions.artifacts import Artifacts, AsyncArtifacts


class AgentResultArtifacts:
"""Beta: look up immutable artifacts from one completed turn."""

def __init__(self, resource: Artifacts, result: AgentTurnResult) -> None:
self._resource = resource
self._session_id = result.session_id
self._turn_id = result.turn_id

def content(
self,
path: str,
*,
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> HttpxBinaryResponseContent:
"""Read one exact turn/path using the SDK's native binary response."""
options: _RequestOptions = {
"extra_headers": extra_headers,
"extra_query": extra_query,
"extra_body": extra_body,
"timeout": timeout,
}
selected = self._find(path, options)
return self._resource.content(selected.id, session_id=self._session_id, **options)

def download(
self,
path: str,
*,
to: str | PathLike[str],
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> SessionArtifact:
"""Download one exact turn/path to an explicit local destination."""
options: _RequestOptions = {
"extra_headers": extra_headers,
"extra_query": extra_query,
"extra_body": extra_body,
"timeout": timeout,
}
selected = self._find(path, options)
with self._resource.with_streaming_response.content(
selected.id,
session_id=self._session_id,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
) as content:
content.stream_to_file(to)
return selected

def _find(self, path: str, options: _RequestOptions) -> SessionArtifact:
selected: SessionArtifact | None = None
for artifact in self._resource.list(
self._session_id,
**options,
):
if artifact.session_id == self._session_id and artifact.turn_id == self._turn_id and artifact.path == path:
if selected is not None:
raise ValueError("More than one artifact matches this turn and path")
selected = artifact
if selected is None:
raise ValueError("No artifact matches this turn and path")
return selected


class AsyncAgentResultArtifacts:
"""Beta: async artifact downloads from one completed turn."""

def __init__(self, resource: AsyncArtifacts, result: AgentTurnResult) -> None:
self._resource = resource
self._session_id = result.session_id
self._turn_id = result.turn_id

async def content(
self,
path: str,
*,
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> HttpxBinaryResponseContent:
"""Read one exact turn/path using the SDK's native binary response."""
options: _RequestOptions = {
"extra_headers": extra_headers,
"extra_query": extra_query,
"extra_body": extra_body,
"timeout": timeout,
}
selected = await self._find(path, options)
return await self._resource.content(selected.id, session_id=self._session_id, **options)

async def download(
self,
path: str,
*,
to: str | PathLike[str],
extra_headers: Headers | None = None,
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx2.Timeout | None | NotGiven = not_given,
) -> SessionArtifact:
"""Download one exact turn/path to an explicit local destination."""
options: _RequestOptions = {
"extra_headers": extra_headers,
"extra_query": extra_query,
"extra_body": extra_body,
"timeout": timeout,
}
selected = await self._find(path, options)
async with self._resource.with_streaming_response.content(
selected.id,
session_id=self._session_id,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
) as content:
await content.stream_to_file(to)
Comment thread
apcha-oai marked this conversation as resolved.
return selected

async def _find(self, path: str, options: _RequestOptions) -> SessionArtifact:
selected: SessionArtifact | None = None
async for artifact in self._resource.list(
self._session_id,
**options,
):
if artifact.session_id == self._session_id and artifact.turn_id == self._turn_id and artifact.path == path:
if selected is not None:
raise ValueError("More than one artifact matches this turn and path")
selected = artifact
if selected is None:
raise ValueError("No artifact matches this turn and path")
return selected
Loading
Loading