Skip to content

Commit d489113

Browse files
declan-scaleclaude
andcommitted
fix(cli): wire OPENAI_API_KEY and serialize turns in codex templates
Round-5 Greptile review (parity with the claude-code fixes): - default/sync/temporal codex manifests now map OPENAI_API_KEY (the key the codex CLI actually reads) instead of LITELLM_API_KEY, and no longer set an empty-string env value that would shadow it at runtime. - temporal-codex workflow serializes signal turns with an asyncio.Lock so overlapping messages don't race on _codex_thread_id and fork the session. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ace66e2 commit d489113

4 files changed

Lines changed: 69 additions & 54 deletions

File tree

src/agentex/lib/cli/templates/default-codex/manifest.yaml.j2

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ agent:
7676
# Maps Kubernetes secrets to environment variables
7777
# Common credentials include:
7878
credentials:
79-
- env_var_name: LITELLM_API_KEY
80-
secret_name: litellm-api-key
79+
# The codex CLI (`codex exec`) reads OPENAI_API_KEY directly; it does not
80+
# use a LiteLLM key.
81+
- env_var_name: OPENAI_API_KEY
82+
secret_name: openai-api-key
8183
secret_key: api-key
8284
- env_var_name: SGP_API_KEY
8385
secret_name: sgp-api-key
@@ -87,9 +89,10 @@ agent:
8789
secret_key: url
8890

8991
# Optional: Set Environment variables for running your agent locally as well
90-
# as for deployment later on
91-
env:
92-
LITELLM_API_KEY: "" # Set your LLM API key
92+
# as for deployment later on. OPENAI_API_KEY is supplied via the credential
93+
# mapping above (deploy) or your local .env. Do NOT set it to an empty string
94+
# here — that would shadow the real key at runtime.
95+
env: {}
9396
# OPENAI_BASE_URL: "<YOUR_OPENAI_BASE_URL_HERE>"
9497

9598
# Deployment Configuration

src/agentex/lib/cli/templates/sync-codex/manifest.yaml.j2

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,20 @@ agent:
7575
# Maps Kubernetes secrets to environment variables
7676
# Common credentials include:
7777
credentials:
78-
- env_var_name: LITELLM_API_KEY
79-
secret_name: litellm-api-key
78+
# The codex CLI (`codex exec`) reads OPENAI_API_KEY directly; it does not
79+
# use a LiteLLM key.
80+
- env_var_name: OPENAI_API_KEY
81+
secret_name: openai-api-key
8082
secret_key: api-key
8183
- env_var_name: SGP_API_KEY
8284
secret_name: sgp-api-key
8385
secret_key: api-key
8486

8587
# Optional: Set Environment variables for running your agent locally as well
86-
# as for deployment later on
87-
env:
88-
LITELLM_API_KEY: "" # Set your LLM API key
88+
# as for deployment later on. OPENAI_API_KEY is supplied via the credential
89+
# mapping above (deploy) or your local .env. Do NOT set it to an empty string
90+
# here — that would shadow the real key at runtime.
91+
env: {}
8992
# OPENAI_BASE_URL: "<YOUR_OPENAI_BASE_URL_HERE>"
9093

9194

src/agentex/lib/cli/templates/temporal-codex/manifest.yaml.j2

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ agent:
100100
- env_var_name: REDIS_URL
101101
secret_name: redis-url-secret
102102
secret_key: url
103-
# - env_var_name: LITELLM_API_KEY
104-
# secret_name: litellm-api-key
105-
# secret_key: api-key
106-
103+
# The codex CLI spawned in project/activities.py reads OPENAI_API_KEY
104+
# directly; without it every turn fails with an auth error.
105+
- env_var_name: OPENAI_API_KEY
106+
secret_name: openai-api-key
107+
secret_key: api-key
108+
107109
# Optional: Set Environment variables for running your agent locally as well
108110
# as for deployment later on
109111
env: {}

src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ KEY CONCEPTS DEMONSTRATED:
2121
from __future__ import annotations
2222

2323
import os
24+
import asyncio
2425
from datetime import timedelta
2526

2627
from temporalio import workflow
@@ -72,51 +73,57 @@ class {{ workflow_class }}(BaseWorkflow):
7273
self._complete_task = False
7374
self._turn_number = 0
7475
self._codex_thread_id: str | None = None
76+
# Serialize turns: signal handlers can interleave at await points, so two
77+
# quick messages could both read the same stale _codex_thread_id and fork
78+
# the codex session. The lock keeps turns sequential and preserves
79+
# conversation continuity.
80+
self._turn_lock = asyncio.Lock()
7581

7682
@workflow.signal(name=SignalName.RECEIVE_EVENT)
7783
async def on_task_event_send(self, params: SendEventParams) -> None:
7884
"""Handle a new user message: spawn codex, stream events via UnifiedEmitter."""
7985
logger.info("Received task event: %s", params.task.id)
80-
self._turn_number += 1
81-
82-
await adk.messages.create(task_id=params.task.id, content=params.event.content)
83-
84-
user_message = params.event.content.content
85-
86-
async with adk.tracing.span(
87-
trace_id=params.task.id,
88-
task_id=params.task.id,
89-
name=f"Turn {self._turn_number}",
90-
input={"message": user_message},
91-
) as span:
92-
# Delegate the subprocess turn to an activity: subprocess I/O is not
93-
# permitted on the Temporal workflow event loop. The activity streams
94-
# events to the task and returns the final text + codex thread id.
95-
# workflow.now() gives a deterministic timestamp under replay.
96-
result = await workflow.execute_activity(
97-
run_codex_turn,
98-
RunCodexTurnParams(
99-
task_id=params.task.id,
100-
prompt=user_message,
101-
model=MODEL,
102-
trace_id=params.task.id,
103-
parent_span_id=span.id if span else None,
104-
thread_id=self._codex_thread_id,
105-
created_at=workflow.now(),
106-
),
107-
start_to_close_timeout=timedelta(minutes=5),
108-
)
109-
110-
# Persist the codex thread id so the next turn resumes the session.
111-
session_id = result.get("session_id")
112-
if session_id:
113-
self._codex_thread_id = session_id
114-
115-
if span:
116-
span.output = {
117-
"final_text": result.get("final_text"),
118-
"model": result.get("model"),
119-
}
86+
async with self._turn_lock:
87+
self._turn_number += 1
88+
89+
await adk.messages.create(task_id=params.task.id, content=params.event.content)
90+
91+
user_message = params.event.content.content
92+
93+
async with adk.tracing.span(
94+
trace_id=params.task.id,
95+
task_id=params.task.id,
96+
name=f"Turn {self._turn_number}",
97+
input={"message": user_message},
98+
) as span:
99+
# Delegate the subprocess turn to an activity: subprocess I/O is not
100+
# permitted on the Temporal workflow event loop. The activity streams
101+
# events to the task and returns the final text + codex thread id.
102+
# workflow.now() gives a deterministic timestamp under replay.
103+
result = await workflow.execute_activity(
104+
run_codex_turn,
105+
RunCodexTurnParams(
106+
task_id=params.task.id,
107+
prompt=user_message,
108+
model=MODEL,
109+
trace_id=params.task.id,
110+
parent_span_id=span.id if span else None,
111+
thread_id=self._codex_thread_id,
112+
created_at=workflow.now(),
113+
),
114+
start_to_close_timeout=timedelta(minutes=5),
115+
)
116+
117+
# Persist the codex thread id so the next turn resumes the session.
118+
session_id = result.get("session_id")
119+
if session_id:
120+
self._codex_thread_id = session_id
121+
122+
if span:
123+
span.output = {
124+
"final_text": result.get("final_text"),
125+
"model": result.get("model"),
126+
}
120127

121128
@workflow.run
122129
async def on_task_create(self, params: CreateTaskParams) -> str:

0 commit comments

Comments
 (0)