temporalio-openai-codex 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- temporalio_openai_codex-0.0.1/LICENSE +21 -0
- temporalio_openai_codex-0.0.1/PKG-INFO +227 -0
- temporalio_openai_codex-0.0.1/README.md +203 -0
- temporalio_openai_codex-0.0.1/pyproject.toml +124 -0
- temporalio_openai_codex-0.0.1/pyproject.toml.orig +108 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/__init__.py +24 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_activities.py +223 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_client.py +715 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_compat.py +103 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_constants.py +11 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_errors.py +46 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_heartbeat_decorator.py +36 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_plugin.py +127 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_routing.py +194 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_subprocess.py +148 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/_types.py +118 -0
- temporalio_openai_codex-0.0.1/src/temporalio/openai_codex/py.typed +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 temporal.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: temporalio-openai-codex
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Durable OpenAI Codex sessions on Temporal
|
|
5
|
+
Author: Temporal Technologies Inc
|
|
6
|
+
Author-email: Temporal Technologies Inc <sdk@temporal.io>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: temporalio>=1.34.0,<2
|
|
17
|
+
Requires-Dist: openai-codex>=0.161.0,<0.164.0
|
|
18
|
+
Requires-Dist: pydantic>=2.12,<3
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Project-URL: Homepage, https://github.com/temporalio/ai-integrations/tree/main/python/openai_codex
|
|
21
|
+
Project-URL: Repository, https://github.com/temporalio/ai-integrations
|
|
22
|
+
Project-URL: Documentation, https://docs.temporal.io/develop/python/integrations/openai-codex
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Temporal OpenAI Codex integration
|
|
26
|
+
|
|
27
|
+
**Pre-release.** Run the [Python Codex SDK](https://github.com/openai/codex/tree/main/sdk/python)
|
|
28
|
+
in Temporal Workflows. Install with
|
|
29
|
+
`uv add temporalio-openai-codex`. Import the integration as
|
|
30
|
+
`temporalio.openai_codex` and the upstream SDK as `openai_codex`.
|
|
31
|
+
|
|
32
|
+
Each SDK keeps its exact `openai-codex-cli-bin` dependency. The
|
|
33
|
+
default connector uses that bundled runtime and its companion PATH entries,
|
|
34
|
+
including when another Codex executable is installed on the Worker.
|
|
35
|
+
|
|
36
|
+
## Use AsyncCodex
|
|
37
|
+
|
|
38
|
+
Register the plugin on the Worker (or its Client):
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from temporalio.openai_codex import OpenAICodexPlugin
|
|
42
|
+
from temporalio.worker import Worker
|
|
43
|
+
|
|
44
|
+
worker = Worker(
|
|
45
|
+
client,
|
|
46
|
+
task_queue="codex",
|
|
47
|
+
workflows=[CodingWorkflow],
|
|
48
|
+
plugins=[OpenAICodexPlugin()],
|
|
49
|
+
)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use the upstream async API in the Workflow:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from openai_codex import AsyncCodex, Sandbox
|
|
56
|
+
from temporalio import workflow
|
|
57
|
+
from temporalio.openai_codex import TemporalCodexConfig
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@workflow.defn
|
|
61
|
+
class CodingWorkflow:
|
|
62
|
+
@workflow.run
|
|
63
|
+
async def run(self, prompt: str) -> str:
|
|
64
|
+
async with AsyncCodex(TemporalCodexConfig(cwd="/workspace")) as codex:
|
|
65
|
+
thread = await codex.thread_start(sandbox=Sandbox.read_only)
|
|
66
|
+
result = await thread.run(prompt)
|
|
67
|
+
return result.final_response or ""
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The plugin configures passthrough imports for `openai_codex` and
|
|
71
|
+
`temporalio.openai_codex`, so Workflow code can import both directly.
|
|
72
|
+
|
|
73
|
+
The plugin adapts construction and asynchronous turn streams inside Workflows,
|
|
74
|
+
including replay. Threads, turn handles, login handles, notifications, and
|
|
75
|
+
`TurnResult` are the upstream types. Ordinary SDK calls outside Workflows keep
|
|
76
|
+
using upstream implementations. Synchronous `Codex` is for Activity or application
|
|
77
|
+
code, not Workflow code.
|
|
78
|
+
|
|
79
|
+
In Workflows, collected failed turns raise `CodexError` with the upstream error
|
|
80
|
+
message, so an uncaught provider failure fails the Workflow. Streaming still yields
|
|
81
|
+
the upstream turn-completion notification, including its failure status and error.
|
|
82
|
+
|
|
83
|
+
## Approvals and Activity options
|
|
84
|
+
|
|
85
|
+
Pass `TemporalCodexConfig`, a subclass of the upstream `CodexConfig`, to
|
|
86
|
+
`AsyncCodex` for Workflow-specific options:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from datetime import timedelta
|
|
90
|
+
from openai_codex import AsyncCodex
|
|
91
|
+
from temporalio.openai_codex import CodexActivityOptions, TemporalCodexConfig
|
|
92
|
+
from temporalio.workflow import ActivityConfig
|
|
93
|
+
|
|
94
|
+
async def approve(method, params):
|
|
95
|
+
# Runs in Workflow code. It can wait for a Signal or Update, or call an Activity.
|
|
96
|
+
return {"decision": "accept"}
|
|
97
|
+
|
|
98
|
+
async with AsyncCodex(TemporalCodexConfig(
|
|
99
|
+
cwd="/workspace",
|
|
100
|
+
approval_handler=approve,
|
|
101
|
+
activity_options=CodexActivityOptions(
|
|
102
|
+
connect=ActivityConfig(start_to_close_timeout=timedelta(minutes=2)),
|
|
103
|
+
),
|
|
104
|
+
)) as codex:
|
|
105
|
+
thread = await codex.thread_start()
|
|
106
|
+
result = await thread.run("Describe the project.")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Synchronous and asynchronous approval handlers receive `(method, params)` as in
|
|
110
|
+
upstream `CodexClient`. Keep callbacks deterministic; put I/O in Activities. Without
|
|
111
|
+
a handler, command and file-change approval requests receive `{"decision": "accept"}`
|
|
112
|
+
and other server requests receive `{}`, matching the SDK default. Set Codex's
|
|
113
|
+
approval and sandbox policies deliberately for your application. These additional
|
|
114
|
+
options apply inside Workflows; ordinary SDK calls use upstream behavior.
|
|
115
|
+
|
|
116
|
+
Only the base `CodexConfig` fields are passed to the connect Activity. Approval
|
|
117
|
+
callbacks, Activity options, and stream topic handles stay in Workflow code.
|
|
118
|
+
|
|
119
|
+
## Token-by-Token Output
|
|
120
|
+
|
|
121
|
+
Use the optional `stream` topic to expose generated output to callers outside the
|
|
122
|
+
Workflow. [Workflow Streams](https://docs.temporal.io/develop/python/workflows/workflow-streams)
|
|
123
|
+
is currently in Public Preview. Create the stream in `@workflow.init`, then pass
|
|
124
|
+
its topic in `TemporalCodexConfig`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from openai_codex import AsyncCodex
|
|
128
|
+
from temporalio import workflow
|
|
129
|
+
from temporalio.contrib.workflow_streams import WorkflowStream
|
|
130
|
+
from temporalio.openai_codex import TemporalCodexConfig
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@workflow.defn
|
|
134
|
+
class StreamingWorkflow:
|
|
135
|
+
@workflow.init
|
|
136
|
+
def __init__(self, prompt: str) -> None:
|
|
137
|
+
self.events = WorkflowStream().topic("codex")
|
|
138
|
+
|
|
139
|
+
@workflow.run
|
|
140
|
+
async def run(self, prompt: str) -> str:
|
|
141
|
+
async with AsyncCodex(TemporalCodexConfig(stream=self.events)) as codex:
|
|
142
|
+
thread = await codex.thread_start()
|
|
143
|
+
result = await thread.run(prompt)
|
|
144
|
+
return result.final_response or ""
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
An external caller subscribes using the Workflow ID:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from temporalio.contrib.workflow_streams import WorkflowStreamClient
|
|
151
|
+
|
|
152
|
+
stream = WorkflowStreamClient.create(client, workflow_id)
|
|
153
|
+
async for item in stream.subscribe(["codex"], result_type=dict):
|
|
154
|
+
notification = item.data
|
|
155
|
+
print(notification["method"], notification["params"])
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Each item is a JSON-RPC delta notification with `method` and `params`, including
|
|
159
|
+
thread, turn, and item IDs when provided by Codex. Notifications whose method ends
|
|
160
|
+
in `delta` (case insensitive) are published once by the connection's reader,
|
|
161
|
+
including agent text, reasoning, and command output. They remain available through
|
|
162
|
+
upstream `turn.stream()` as well. Activity retries and multiple turn subscribers
|
|
163
|
+
do not duplicate these publications. Explicitly repeating a turn can generate
|
|
164
|
+
new output; consumers should use the IDs to associate it with the correct turn.
|
|
165
|
+
Streaming adds Workflow memory use; apply the Workflow Streams retention guidance
|
|
166
|
+
for long conversations.
|
|
167
|
+
|
|
168
|
+
## Connectors and launch configuration
|
|
169
|
+
|
|
170
|
+
`OpenAICodexPlugin(connector=...)` accepts a `Connector` implementing
|
|
171
|
+
`async connect(ConnectActivityInput) -> CodexProcess`. The input contains the upstream
|
|
172
|
+
`CodexConfig`. `CodexProcess` provides `write(message)`, `read() -> list[dict]`, and
|
|
173
|
+
`close()`. A read returns an ordered batch or an empty list at EOF. Close must be
|
|
174
|
+
idempotent, tolerate overlap, support retries after cancellation, and unblock
|
|
175
|
+
pending reads and writes. A transient
|
|
176
|
+
write failure must guarantee that the message was not committed; report ambiguous
|
|
177
|
+
or permanent failures with `CodexError` so they are not blindly retried.
|
|
178
|
+
|
|
179
|
+
The default `SubprocessConnector` honors `codex_bin`, `launch_args_override`,
|
|
180
|
+
`config_overrides`, `cwd`, and `env`. It merges `env` into the Worker's environment.
|
|
181
|
+
Launch paths and runtime discovery are resolved on the Activity Worker. Custom
|
|
182
|
+
connectors can reach a remote process; the returned connection object still lives
|
|
183
|
+
on the Worker that created it. The connector owns cleanup of remote resources
|
|
184
|
+
that outlive the Worker. No Nexus service or tool-registration API is needed.
|
|
185
|
+
|
|
186
|
+
`CodexActivityOptions` overrides `connect`, `write`, `read`, and `close`, each using
|
|
187
|
+
the Temporal SDK's `workflow.ActivityConfig`. Only
|
|
188
|
+
`connect.task_queue` selects the queue where Codex starts. The other Activities use
|
|
189
|
+
the private queue polled by the Worker holding the connection. Activities default
|
|
190
|
+
to five-minute start-to-close timeouts and ten-second heartbeat timeouts. Operations
|
|
191
|
+
on an established connection also have a thirty-second schedule-to-start timeout
|
|
192
|
+
for detecting a lost Worker.
|
|
193
|
+
|
|
194
|
+
## Recovery and operational limits
|
|
195
|
+
|
|
196
|
+
Completed writes stay cached for the life of the connection, so Activity retries
|
|
197
|
+
and lost completion results do not resend prompts, approvals, or notifications.
|
|
198
|
+
Read batches stay buffered until a later read acknowledges them. Heartbeats let
|
|
199
|
+
Temporal cancel blocked operations; context exit closes the process, and orderly
|
|
200
|
+
Worker shutdown closes its connections.
|
|
201
|
+
If a connect result is lost, its idle connection remains with the owning Worker
|
|
202
|
+
until shutdown. Cleanup failures do not replace a Workflow's result or original error.
|
|
203
|
+
|
|
204
|
+
A process connection is not durable storage. Losing its Worker produces
|
|
205
|
+
`TransportClosedError`; the plugin does not automatically start a replacement
|
|
206
|
+
session. Keep persisted thread IDs in Workflow state. Handle the error by creating
|
|
207
|
+
a new client and explicitly calling `thread_resume` with the saved ID, using a
|
|
208
|
+
Worker with access to the same Codex home and workspace. Ephemeral threads and
|
|
209
|
+
in-flight process state cannot be recovered this way. Retrying a Workflow or
|
|
210
|
+
resetting its history can create new sessions and repeat application side effects.
|
|
211
|
+
|
|
212
|
+
Configure credentials on Workers, such as an authenticated `CODEX_HOME` or an
|
|
213
|
+
appropriate model provider. Avoid passing secrets through Workflow inputs,
|
|
214
|
+
`TemporalCodexConfig.env`, or login API-key calls: Activity inputs/results and
|
|
215
|
+
notification content are recorded in Temporal history. Use an appropriate payload
|
|
216
|
+
codec if sensitive content must pass through history. Tests require no real credentials.
|
|
217
|
+
|
|
218
|
+
Each protocol write and output batch adds Activity events to Workflow history.
|
|
219
|
+
Long streams and long-lived sessions grow history; bound conversations and close
|
|
220
|
+
connections before continuing as new. Worker memory also holds unacknowledged
|
|
221
|
+
output and write-deduplication records until close. Reads batch near Temporal's
|
|
222
|
+
payload warning threshold; a single oversized message is not split and can exceed
|
|
223
|
+
the server's payload limit. Use upstream options to limit output and an external
|
|
224
|
+
storage/payload strategy where needed. The subprocess reader accepts lines up to
|
|
225
|
+
16 MiB; the Temporal payload limit is usually smaller. Exceeding that line limit
|
|
226
|
+
or another terminal stdout read failure raises `TransportClosedError` without
|
|
227
|
+
retrying the failed reader.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Temporal OpenAI Codex integration
|
|
2
|
+
|
|
3
|
+
**Pre-release.** Run the [Python Codex SDK](https://github.com/openai/codex/tree/main/sdk/python)
|
|
4
|
+
in Temporal Workflows. Install with
|
|
5
|
+
`uv add temporalio-openai-codex`. Import the integration as
|
|
6
|
+
`temporalio.openai_codex` and the upstream SDK as `openai_codex`.
|
|
7
|
+
|
|
8
|
+
Each SDK keeps its exact `openai-codex-cli-bin` dependency. The
|
|
9
|
+
default connector uses that bundled runtime and its companion PATH entries,
|
|
10
|
+
including when another Codex executable is installed on the Worker.
|
|
11
|
+
|
|
12
|
+
## Use AsyncCodex
|
|
13
|
+
|
|
14
|
+
Register the plugin on the Worker (or its Client):
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from temporalio.openai_codex import OpenAICodexPlugin
|
|
18
|
+
from temporalio.worker import Worker
|
|
19
|
+
|
|
20
|
+
worker = Worker(
|
|
21
|
+
client,
|
|
22
|
+
task_queue="codex",
|
|
23
|
+
workflows=[CodingWorkflow],
|
|
24
|
+
plugins=[OpenAICodexPlugin()],
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use the upstream async API in the Workflow:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from openai_codex import AsyncCodex, Sandbox
|
|
32
|
+
from temporalio import workflow
|
|
33
|
+
from temporalio.openai_codex import TemporalCodexConfig
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@workflow.defn
|
|
37
|
+
class CodingWorkflow:
|
|
38
|
+
@workflow.run
|
|
39
|
+
async def run(self, prompt: str) -> str:
|
|
40
|
+
async with AsyncCodex(TemporalCodexConfig(cwd="/workspace")) as codex:
|
|
41
|
+
thread = await codex.thread_start(sandbox=Sandbox.read_only)
|
|
42
|
+
result = await thread.run(prompt)
|
|
43
|
+
return result.final_response or ""
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The plugin configures passthrough imports for `openai_codex` and
|
|
47
|
+
`temporalio.openai_codex`, so Workflow code can import both directly.
|
|
48
|
+
|
|
49
|
+
The plugin adapts construction and asynchronous turn streams inside Workflows,
|
|
50
|
+
including replay. Threads, turn handles, login handles, notifications, and
|
|
51
|
+
`TurnResult` are the upstream types. Ordinary SDK calls outside Workflows keep
|
|
52
|
+
using upstream implementations. Synchronous `Codex` is for Activity or application
|
|
53
|
+
code, not Workflow code.
|
|
54
|
+
|
|
55
|
+
In Workflows, collected failed turns raise `CodexError` with the upstream error
|
|
56
|
+
message, so an uncaught provider failure fails the Workflow. Streaming still yields
|
|
57
|
+
the upstream turn-completion notification, including its failure status and error.
|
|
58
|
+
|
|
59
|
+
## Approvals and Activity options
|
|
60
|
+
|
|
61
|
+
Pass `TemporalCodexConfig`, a subclass of the upstream `CodexConfig`, to
|
|
62
|
+
`AsyncCodex` for Workflow-specific options:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from datetime import timedelta
|
|
66
|
+
from openai_codex import AsyncCodex
|
|
67
|
+
from temporalio.openai_codex import CodexActivityOptions, TemporalCodexConfig
|
|
68
|
+
from temporalio.workflow import ActivityConfig
|
|
69
|
+
|
|
70
|
+
async def approve(method, params):
|
|
71
|
+
# Runs in Workflow code. It can wait for a Signal or Update, or call an Activity.
|
|
72
|
+
return {"decision": "accept"}
|
|
73
|
+
|
|
74
|
+
async with AsyncCodex(TemporalCodexConfig(
|
|
75
|
+
cwd="/workspace",
|
|
76
|
+
approval_handler=approve,
|
|
77
|
+
activity_options=CodexActivityOptions(
|
|
78
|
+
connect=ActivityConfig(start_to_close_timeout=timedelta(minutes=2)),
|
|
79
|
+
),
|
|
80
|
+
)) as codex:
|
|
81
|
+
thread = await codex.thread_start()
|
|
82
|
+
result = await thread.run("Describe the project.")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Synchronous and asynchronous approval handlers receive `(method, params)` as in
|
|
86
|
+
upstream `CodexClient`. Keep callbacks deterministic; put I/O in Activities. Without
|
|
87
|
+
a handler, command and file-change approval requests receive `{"decision": "accept"}`
|
|
88
|
+
and other server requests receive `{}`, matching the SDK default. Set Codex's
|
|
89
|
+
approval and sandbox policies deliberately for your application. These additional
|
|
90
|
+
options apply inside Workflows; ordinary SDK calls use upstream behavior.
|
|
91
|
+
|
|
92
|
+
Only the base `CodexConfig` fields are passed to the connect Activity. Approval
|
|
93
|
+
callbacks, Activity options, and stream topic handles stay in Workflow code.
|
|
94
|
+
|
|
95
|
+
## Token-by-Token Output
|
|
96
|
+
|
|
97
|
+
Use the optional `stream` topic to expose generated output to callers outside the
|
|
98
|
+
Workflow. [Workflow Streams](https://docs.temporal.io/develop/python/workflows/workflow-streams)
|
|
99
|
+
is currently in Public Preview. Create the stream in `@workflow.init`, then pass
|
|
100
|
+
its topic in `TemporalCodexConfig`:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from openai_codex import AsyncCodex
|
|
104
|
+
from temporalio import workflow
|
|
105
|
+
from temporalio.contrib.workflow_streams import WorkflowStream
|
|
106
|
+
from temporalio.openai_codex import TemporalCodexConfig
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@workflow.defn
|
|
110
|
+
class StreamingWorkflow:
|
|
111
|
+
@workflow.init
|
|
112
|
+
def __init__(self, prompt: str) -> None:
|
|
113
|
+
self.events = WorkflowStream().topic("codex")
|
|
114
|
+
|
|
115
|
+
@workflow.run
|
|
116
|
+
async def run(self, prompt: str) -> str:
|
|
117
|
+
async with AsyncCodex(TemporalCodexConfig(stream=self.events)) as codex:
|
|
118
|
+
thread = await codex.thread_start()
|
|
119
|
+
result = await thread.run(prompt)
|
|
120
|
+
return result.final_response or ""
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
An external caller subscribes using the Workflow ID:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from temporalio.contrib.workflow_streams import WorkflowStreamClient
|
|
127
|
+
|
|
128
|
+
stream = WorkflowStreamClient.create(client, workflow_id)
|
|
129
|
+
async for item in stream.subscribe(["codex"], result_type=dict):
|
|
130
|
+
notification = item.data
|
|
131
|
+
print(notification["method"], notification["params"])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Each item is a JSON-RPC delta notification with `method` and `params`, including
|
|
135
|
+
thread, turn, and item IDs when provided by Codex. Notifications whose method ends
|
|
136
|
+
in `delta` (case insensitive) are published once by the connection's reader,
|
|
137
|
+
including agent text, reasoning, and command output. They remain available through
|
|
138
|
+
upstream `turn.stream()` as well. Activity retries and multiple turn subscribers
|
|
139
|
+
do not duplicate these publications. Explicitly repeating a turn can generate
|
|
140
|
+
new output; consumers should use the IDs to associate it with the correct turn.
|
|
141
|
+
Streaming adds Workflow memory use; apply the Workflow Streams retention guidance
|
|
142
|
+
for long conversations.
|
|
143
|
+
|
|
144
|
+
## Connectors and launch configuration
|
|
145
|
+
|
|
146
|
+
`OpenAICodexPlugin(connector=...)` accepts a `Connector` implementing
|
|
147
|
+
`async connect(ConnectActivityInput) -> CodexProcess`. The input contains the upstream
|
|
148
|
+
`CodexConfig`. `CodexProcess` provides `write(message)`, `read() -> list[dict]`, and
|
|
149
|
+
`close()`. A read returns an ordered batch or an empty list at EOF. Close must be
|
|
150
|
+
idempotent, tolerate overlap, support retries after cancellation, and unblock
|
|
151
|
+
pending reads and writes. A transient
|
|
152
|
+
write failure must guarantee that the message was not committed; report ambiguous
|
|
153
|
+
or permanent failures with `CodexError` so they are not blindly retried.
|
|
154
|
+
|
|
155
|
+
The default `SubprocessConnector` honors `codex_bin`, `launch_args_override`,
|
|
156
|
+
`config_overrides`, `cwd`, and `env`. It merges `env` into the Worker's environment.
|
|
157
|
+
Launch paths and runtime discovery are resolved on the Activity Worker. Custom
|
|
158
|
+
connectors can reach a remote process; the returned connection object still lives
|
|
159
|
+
on the Worker that created it. The connector owns cleanup of remote resources
|
|
160
|
+
that outlive the Worker. No Nexus service or tool-registration API is needed.
|
|
161
|
+
|
|
162
|
+
`CodexActivityOptions` overrides `connect`, `write`, `read`, and `close`, each using
|
|
163
|
+
the Temporal SDK's `workflow.ActivityConfig`. Only
|
|
164
|
+
`connect.task_queue` selects the queue where Codex starts. The other Activities use
|
|
165
|
+
the private queue polled by the Worker holding the connection. Activities default
|
|
166
|
+
to five-minute start-to-close timeouts and ten-second heartbeat timeouts. Operations
|
|
167
|
+
on an established connection also have a thirty-second schedule-to-start timeout
|
|
168
|
+
for detecting a lost Worker.
|
|
169
|
+
|
|
170
|
+
## Recovery and operational limits
|
|
171
|
+
|
|
172
|
+
Completed writes stay cached for the life of the connection, so Activity retries
|
|
173
|
+
and lost completion results do not resend prompts, approvals, or notifications.
|
|
174
|
+
Read batches stay buffered until a later read acknowledges them. Heartbeats let
|
|
175
|
+
Temporal cancel blocked operations; context exit closes the process, and orderly
|
|
176
|
+
Worker shutdown closes its connections.
|
|
177
|
+
If a connect result is lost, its idle connection remains with the owning Worker
|
|
178
|
+
until shutdown. Cleanup failures do not replace a Workflow's result or original error.
|
|
179
|
+
|
|
180
|
+
A process connection is not durable storage. Losing its Worker produces
|
|
181
|
+
`TransportClosedError`; the plugin does not automatically start a replacement
|
|
182
|
+
session. Keep persisted thread IDs in Workflow state. Handle the error by creating
|
|
183
|
+
a new client and explicitly calling `thread_resume` with the saved ID, using a
|
|
184
|
+
Worker with access to the same Codex home and workspace. Ephemeral threads and
|
|
185
|
+
in-flight process state cannot be recovered this way. Retrying a Workflow or
|
|
186
|
+
resetting its history can create new sessions and repeat application side effects.
|
|
187
|
+
|
|
188
|
+
Configure credentials on Workers, such as an authenticated `CODEX_HOME` or an
|
|
189
|
+
appropriate model provider. Avoid passing secrets through Workflow inputs,
|
|
190
|
+
`TemporalCodexConfig.env`, or login API-key calls: Activity inputs/results and
|
|
191
|
+
notification content are recorded in Temporal history. Use an appropriate payload
|
|
192
|
+
codec if sensitive content must pass through history. Tests require no real credentials.
|
|
193
|
+
|
|
194
|
+
Each protocol write and output batch adds Activity events to Workflow history.
|
|
195
|
+
Long streams and long-lived sessions grow history; bound conversations and close
|
|
196
|
+
connections before continuing as new. Worker memory also holds unacknowledged
|
|
197
|
+
output and write-deduplication records until close. Reads batch near Temporal's
|
|
198
|
+
payload warning threshold; a single oversized message is not split and can exceed
|
|
199
|
+
the server's payload limit. Use upstream options to limit output and an external
|
|
200
|
+
storage/payload strategy where needed. The subprocess reader accepts lines up to
|
|
201
|
+
16 MiB; the Temporal payload limit is usually smaller. Exceeding that line limit
|
|
202
|
+
or another terminal stdout read failure raises `TransportClosedError` without
|
|
203
|
+
retrying the failed reader.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "temporalio-openai-codex"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Durable OpenAI Codex sessions on Temporal"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Programming Language :: Python :: 3.10",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"temporalio>=1.34.0,<2",
|
|
20
|
+
"openai-codex>=0.161.0,<0.164.0",
|
|
21
|
+
"pydantic>=2.12,<3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[[project.authors]]
|
|
25
|
+
name = "Temporal Technologies Inc"
|
|
26
|
+
email = "sdk@temporal.io"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/temporalio/ai-integrations/tree/main/python/openai_codex"
|
|
32
|
+
Repository = "https://github.com/temporalio/ai-integrations"
|
|
33
|
+
Documentation = "https://docs.temporal.io/develop/python/integrations/openai-codex"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.12.5,<0.13"]
|
|
37
|
+
build-backend = "uv_build"
|
|
38
|
+
|
|
39
|
+
[tool.uv]
|
|
40
|
+
default-groups = ["dev"]
|
|
41
|
+
required-version = ">=0.12.5,<0.13"
|
|
42
|
+
exclude-newer = "2 weeks"
|
|
43
|
+
link-mode = "copy"
|
|
44
|
+
|
|
45
|
+
[tool.uv.build-backend]
|
|
46
|
+
module-name = "temporalio.openai_codex"
|
|
47
|
+
|
|
48
|
+
[tool.uv.exclude-newer-package]
|
|
49
|
+
temporalio = false
|
|
50
|
+
|
|
51
|
+
[[tool.uv.cache-keys]]
|
|
52
|
+
file = "pyproject.toml"
|
|
53
|
+
|
|
54
|
+
[[tool.uv.cache-keys]]
|
|
55
|
+
file = "README.md"
|
|
56
|
+
|
|
57
|
+
[[tool.uv.cache-keys]]
|
|
58
|
+
file = "src/**/*"
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
asyncio_mode = "auto"
|
|
62
|
+
log_level = "DEBUG"
|
|
63
|
+
log_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
timeout = 60
|
|
66
|
+
timeout_func_only = true
|
|
67
|
+
filterwarnings = [
|
|
68
|
+
"error::temporalio.workflow.UnfinishedUpdateHandlersWarning",
|
|
69
|
+
"error::temporalio.workflow.UnfinishedSignalHandlersWarning",
|
|
70
|
+
"ignore::pytest.PytestDeprecationWarning",
|
|
71
|
+
"ignore::DeprecationWarning",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
files = [
|
|
76
|
+
"src",
|
|
77
|
+
"tests",
|
|
78
|
+
]
|
|
79
|
+
mypy_path = "src"
|
|
80
|
+
namespace_packages = true
|
|
81
|
+
explicit_package_bases = true
|
|
82
|
+
check_untyped_defs = true
|
|
83
|
+
ignore_missing_imports = true
|
|
84
|
+
|
|
85
|
+
[tool.pyright]
|
|
86
|
+
extends = "../_shared/pyrightconfig.json"
|
|
87
|
+
include = [
|
|
88
|
+
"src",
|
|
89
|
+
"tests",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[[tool.pyright.executionEnvironments]]
|
|
93
|
+
root = "."
|
|
94
|
+
extraPaths = ["src"]
|
|
95
|
+
|
|
96
|
+
[tool.pydocstyle]
|
|
97
|
+
convention = "google"
|
|
98
|
+
add_ignore = [
|
|
99
|
+
"D205",
|
|
100
|
+
"D415",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[tool.ruff]
|
|
104
|
+
extend = "../_shared/ruff.toml"
|
|
105
|
+
src = [
|
|
106
|
+
".",
|
|
107
|
+
"src",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
[dependency-groups]
|
|
111
|
+
dev = [
|
|
112
|
+
"pyright>=1.1.414,<2",
|
|
113
|
+
"basedpyright>=1.40.2,<2",
|
|
114
|
+
"mypy>=1.20.2,<2",
|
|
115
|
+
"ruff>=0.15.12,<0.16",
|
|
116
|
+
"pydocstyle>=6.3,<7",
|
|
117
|
+
"pytest~=9.0",
|
|
118
|
+
"pytest-asyncio>=0.21.2,<0.22",
|
|
119
|
+
"pytest-timeout~=2.2",
|
|
120
|
+
"pytest-xdist>=3.6,<4",
|
|
121
|
+
"pytest-pretty>=1.3",
|
|
122
|
+
"pytest-rerunfailures>=16.1",
|
|
123
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
124
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "temporalio-openai-codex"
|
|
3
|
+
version = "0.0.1" # Development placeholder; protected release tags inject the published version in CI.
|
|
4
|
+
description = "Durable OpenAI Codex sessions on Temporal"
|
|
5
|
+
authors = [{ name = "Temporal Technologies Inc", email = "sdk@temporal.io" }]
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"] # committed copy of the repository's root LICENSE
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha", # maturity; must agree with plugin.toml (see AGENTS.md for the mapping)
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"temporalio>=1.34.0,<2",
|
|
21
|
+
"openai-codex>=0.161.0,<0.164.0",
|
|
22
|
+
"pydantic>=2.12,<3",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/temporalio/ai-integrations/tree/main/python/openai_codex"
|
|
29
|
+
Repository = "https://github.com/temporalio/ai-integrations"
|
|
30
|
+
Documentation = "https://docs.temporal.io/develop/python/integrations/openai-codex"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.12.5,<0.13"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[tool.uv.build-backend]
|
|
37
|
+
# src/temporalio and src/temporalio/contrib must NOT contain __init__.py: the SDK wheel owns them.
|
|
38
|
+
module-name = "temporalio.openai_codex"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pyright>=1.1.414,<2",
|
|
43
|
+
"basedpyright>=1.40.2,<2",
|
|
44
|
+
"mypy>=1.20.2,<2",
|
|
45
|
+
"ruff>=0.15.12,<0.16",
|
|
46
|
+
"pydocstyle>=6.3,<7",
|
|
47
|
+
"pytest~=9.0",
|
|
48
|
+
"pytest-asyncio>=0.21.2,<0.22",
|
|
49
|
+
"pytest-timeout~=2.2",
|
|
50
|
+
"pytest-xdist>=3.6,<4",
|
|
51
|
+
"pytest-pretty>=1.3",
|
|
52
|
+
"pytest-rerunfailures>=16.1",
|
|
53
|
+
"tomli>=2.0; python_version < '3.11'",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.uv]
|
|
57
|
+
default-groups = ["dev"]
|
|
58
|
+
required-version = ">=0.12.5,<0.13" # same value in scripts/pyproject.toml; CI reads it via setup-uv version-file
|
|
59
|
+
exclude-newer = "2 weeks" # org supply-chain cooldown for third-party packages
|
|
60
|
+
exclude-newer-package = { temporalio = false } # the SDK is exempt: releases must be adoptable on day one
|
|
61
|
+
link-mode = "copy"
|
|
62
|
+
cache-keys = [{ file = "pyproject.toml" }, { file = "README.md" }, { file = "src/**/*" }]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
# Do not use log_cli since this shows logging for all tests, not just the ones
|
|
67
|
+
# that failed. Instead, show all logs for failed tests at the end.
|
|
68
|
+
log_level = "DEBUG"
|
|
69
|
+
log_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
timeout = 60
|
|
72
|
+
timeout_func_only = true
|
|
73
|
+
filterwarnings = [
|
|
74
|
+
"error::temporalio.workflow.UnfinishedUpdateHandlersWarning",
|
|
75
|
+
"error::temporalio.workflow.UnfinishedSignalHandlersWarning",
|
|
76
|
+
"ignore::pytest.PytestDeprecationWarning",
|
|
77
|
+
"ignore::DeprecationWarning",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.mypy]
|
|
81
|
+
files = ["src", "tests"]
|
|
82
|
+
mypy_path = "src"
|
|
83
|
+
namespace_packages = true
|
|
84
|
+
explicit_package_bases = true
|
|
85
|
+
check_untyped_defs = true
|
|
86
|
+
ignore_missing_imports = true
|
|
87
|
+
|
|
88
|
+
[tool.pyright]
|
|
89
|
+
extends = "../_shared/pyrightconfig.json"
|
|
90
|
+
include = ["src", "tests"]
|
|
91
|
+
|
|
92
|
+
# Resolve the plugin's own modules from source rather than from site-packages.
|
|
93
|
+
[[tool.pyright.executionEnvironments]]
|
|
94
|
+
root = "."
|
|
95
|
+
extraPaths = ["src"]
|
|
96
|
+
|
|
97
|
+
[tool.pydocstyle]
|
|
98
|
+
convention = "google"
|
|
99
|
+
add_ignore = [
|
|
100
|
+
# We like to wrap at a certain number of chars, even long summary sentences.
|
|
101
|
+
# https://github.com/PyCQA/pydocstyle/issues/184
|
|
102
|
+
"D205",
|
|
103
|
+
"D415",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
[tool.ruff]
|
|
107
|
+
extend = "../_shared/ruff.toml"
|
|
108
|
+
src = [".", "src"]
|