Skip to content

feat(cli): add default-openai-agents init template (async base)#434

Merged
declan-scale merged 2 commits into
nextfrom
declan-scale/init-template-openai-agents
Jun 24, 2026
Merged

feat(cli): add default-openai-agents init template (async base)#434
declan-scale merged 2 commits into
nextfrom
declan-scale/init-template-openai-agents

Conversation

@declan-scale

@declan-scale declan-scale commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Eighth slice of #425. Adds the missing async-base OpenAI Agents SDK init template.

Test plan

  • pytest tests/lib/cli/ -k "init or template" — 20 passed (template renders to valid Python)

Notes

Stacked on #433. Retarget to next after the chain merges.

🤖 Generated with Claude Code

Greptile Summary

This PR adds the default-openai-agents async template (11 scaffold files) and wires it into the CLI init command as the eighth slice of the template-expansion stack. Compared to the previous review round, several issues have been resolved: SGP tracing is now correctly gated on if _sgp_api_key and _sgp_account_id, the agent is rebuilt per-request so datetime.now() in the instructions stays current, both Dockerfiles correctly use the nodejs apt package, and the notebook streaming cell consistently references task_message_update.

  • init.py: DEFAULT_OPENAI_AGENTS is cleanly added to the enum, project-files map, and async template prompt choices.
  • project/acp.py.j2: Async handler runs the OpenAI Agents SDK via OpenAITurn + UnifiedEmitter.auto_send_turn, persists conversation history in StateModel, and streams output over Redis. StateModel.turn_number is tracked but never read.
  • Dockerfile.j2 / Dockerfile-uv.j2: Pip and uv build variants; the uv variant's dependency on a pre-existing uv.lock is a known open item from the prior review.

Confidence Score: 5/5

The new template is additive and gated behind a CLI prompt — no existing paths are modified. The fixes applied in this revision address the regressions identified earlier.

The changes are entirely new scaffold files plus clean enum/map wiring in init.py. The only newly identified issue in this pass is a dead turn_number field that accumulates unused state but does not affect correctness. No fresh correctness regressions were introduced in this revision.

The dev.ipynb.j2 notebook and Dockerfile-uv.j2 have the most outstanding open items from earlier review rounds.

Important Files Changed

Filename Overview
src/agentex/lib/cli/commands/init.py Adds DEFAULT_OPENAI_AGENTS enum value, project-files map entry, and async prompt choice — straightforward wiring, no issues.
src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 Core async ACP template; SGP guard, datetime freshness, and per-request agent creation are now correct. StateModel.turn_number is incremented but never read. Several structural issues are tracked in earlier review threads.
src/agentex/lib/cli/templates/default-openai-agents/Dockerfile-uv.j2 uv-based Dockerfile; copies uv.lock but no uv.lock.j2 template is generated — tracked in a previous review thread.
src/agentex/lib/cli/templates/default-openai-agents/dev.ipynb.j2 Notebook uses sync message/send helpers against an async-only event/send agent — API mismatch tracked in a previous review thread. Variable references (task_message_update) are now consistent.
src/agentex/lib/cli/templates/default-openai-agents/manifest.yaml.j2 Credentials and env entries are empty/commented — missing required runtime secrets tracked in a previous review thread.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant FastACP as FastACP (async)
    participant Handler as handle_task_event_send
    participant ADK as adk.state / adk.messages
    participant Runner as OpenAI Agents Runner
    participant Emitter as UnifiedEmitter
    participant Redis

    Client->>FastACP: event/send (SendEventParams)
    FastACP->>Handler: dispatch
    Handler->>ADK: messages.create(user content)
    Handler->>ADK: state.get_by_task_and_agent()
    alt state is None
        Handler->>ADK: state.create(StateModel)
    end
    Handler->>Handler: append user turn to input_list
    Handler->>Runner: "Runner.run_streamed(input=input_list)"
    Handler->>Emitter: auto_send_turn(OpenAITurn)
    Emitter->>Runner: iterate streamed events
    Emitter->>Redis: "push StreamTaskMessage* events"
    Emitter-->>Handler: TurnResult
    Handler->>ADK: state.update(result.to_input_list())
    FastACP-->>Client: 202 processing
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant FastACP as FastACP (async)
    participant Handler as handle_task_event_send
    participant ADK as adk.state / adk.messages
    participant Runner as OpenAI Agents Runner
    participant Emitter as UnifiedEmitter
    participant Redis

    Client->>FastACP: event/send (SendEventParams)
    FastACP->>Handler: dispatch
    Handler->>ADK: messages.create(user content)
    Handler->>ADK: state.get_by_task_and_agent()
    alt state is None
        Handler->>ADK: state.create(StateModel)
    end
    Handler->>Handler: append user turn to input_list
    Handler->>Runner: "Runner.run_streamed(input=input_list)"
    Handler->>Emitter: auto_send_turn(OpenAITurn)
    Emitter->>Runner: iterate streamed events
    Emitter->>Redis: "push StreamTaskMessage* events"
    Emitter-->>Handler: TurnResult
    Handler->>ADK: state.update(result.to_input_list())
    FastACP-->>Client: 202 processing
Loading

Comments Outside Diff (1)

  1. General comment

    P1 default-openai-agents init does not render the full promised scaffold artifact set

    • Bug
      • On head, TemplateType.DEFAULT_OPENAI_AGENTS is selectable and the async prompt label is present, but executing create_project_structure(..., TemplateType.DEFAULT_OPENAI_AGENTS, use_uv=True) generated only .dockerignore, .env.example, Dockerfile, README.md, dev.ipynb, environments.yaml, manifest.yaml, project/__init__.py, project/acp.py, and pyproject.toml. The validation expected the changed template artifact set to include both Dockerfile-uv and requirements.txt as scaffold outputs; those were missing, and an extra project/__init__.py was generated relative to the requested exact scaffold assertion.
    • Cause
      • src/agentex/lib/cli/commands/init.py conditionally maps package-management templates: with use_uv=True, Dockerfile-uv.j2 is rendered to Dockerfile and only pyproject.toml is emitted; requirements.txt.j2 and a distinct Dockerfile-uv file are not emitted. The common project directory creation also touches project/__init__.py, so the output cannot match the requested exact 11-file scaffold set.
    • Fix
      • Align the scaffold contract and implementation. If the intended contract is to render all root template artifacts, update create_project_structure to emit Dockerfile-uv, Dockerfile, pyproject.toml, and requirements.txt for DEFAULT_OPENAI_AGENTS as required, and decide whether project/__init__.py should be part of the documented expected set. If package-manager-specific output is intended instead, update the PR contract/tests to expect the conditional file set.

    T-Rex Ran code and verified through T-Rex

Reviews (12): Last reviewed commit: "fix(cli): persist conversation history i..." | Re-trigger Greptile

@declan-scale declan-scale force-pushed the declan-scale/harness-integration-tests branch from 76361ef to 1c5dd14 Compare June 23, 2026 15:28
@declan-scale declan-scale force-pushed the declan-scale/init-template-openai-agents branch from cd6bb70 to ab23d6d Compare June 23, 2026 15:28
@declan-scale

Copy link
Copy Markdown
Contributor Author

@greptile review

Comment thread src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 Outdated
Comment thread src/agentex/lib/cli/templates/default-openai-agents/dev.ipynb.j2 Outdated
Comment thread src/agentex/lib/cli/templates/default-openai-agents/Dockerfile.j2 Outdated
Comment thread src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 Outdated
@declan-scale declan-scale force-pushed the declan-scale/harness-integration-tests branch from 1c5dd14 to 86ce557 Compare June 23, 2026 15:44
@declan-scale declan-scale force-pushed the declan-scale/init-template-openai-agents branch from ab23d6d to d3f1fb6 Compare June 23, 2026 15:44
@declan-scale

Copy link
Copy Markdown
Contributor Author

@greptile review

Comment thread src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 Outdated
@declan-scale

Copy link
Copy Markdown
Contributor Author

@greptile review

agent = get_agent()
task_id = params.task.id
agent_id = params.agent.id
user_message = params.event.content.content

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Non-text events break

This handler assumes every incoming event is a text message by reading params.event.content.content and placing it directly into an OpenAI user input item. SendEventParams.event.content is optional and is the full TaskMessageContent union, so data, tool, or empty events can also reach event/send; for those messages .content may be a dict/list or absent in the expected shape, which creates invalid OpenAI Agents input or raises before the event is processed. Guard for TextContent and explicitly reject, ignore, or convert non-text content before appending to input_list.

Artifacts

Repro: generated handler runtime repro script

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Repro: runtime output showing missing and data event failures

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2
Line: 115

Comment:
**Non-text events break**

This handler assumes every incoming event is a text message by reading `params.event.content.content` and placing it directly into an OpenAI user input item. `SendEventParams.event.content` is optional and is the full `TaskMessageContent` union, so data, tool, or empty events can also reach `event/send`; for those messages `.content` may be a dict/list or absent in the expected shape, which creates invalid OpenAI Agents input or raises before the event is processed. Guard for `TextContent` and explicitly reject, ignore, or convert non-text content before appending to `input_list`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +141 to +153
result = Runner.run_streamed(starting_agent=agent, input=state.input_list)
turn = OpenAITurn(result=result, model=MODEL_NAME)
emitter = UnifiedEmitter(
task_id=task_id,
trace_id=task_id,
parent_span_id=turn_span.id if turn_span else None,
)
turn_result = await emitter.auto_send_turn(turn)

# Persist the full conversation history (user + assistant + tool calls)
# so the next turn resumes with complete context.
state.input_list = result.to_input_list()
await adk.state.update(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed streams lose input

The new user turn is only persisted after auto_send_turn fully exhausts the OpenAI stream. If the model, tool, or emitter stream raises after producing partial output, the handler exits before state.input_list = result.to_input_list() and adk.state.update(...), even though the user's message was already echoed to task history. The next retry or follow-up reloads the old state and re-runs without this accepted user turn, causing duplicate or lost context. Persist the appended user message before starting the run, or update state in a failure path with the best available history.

Artifacts

Repro: generated harness that executes the rendered ACP template with a failing partial stream

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Stack trace captured during the T-Rex run

  • Keeps the raw stack trace available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2
Line: 141-153

Comment:
**Failed streams lose input**

The new user turn is only persisted after `auto_send_turn` fully exhausts the OpenAI stream. If the model, tool, or emitter stream raises after producing partial output, the handler exits before `state.input_list = result.to_input_list()` and `adk.state.update(...)`, even though the user's message was already echoed to task history. The next retry or follow-up reloads the old state and re-runs without this accepted user turn, causing duplicate or lost context. Persist the appended user message before starting the run, or update state in a failure path with the best available history.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

@declan-scale declan-scale force-pushed the declan-scale/harness-integration-tests branch from 86ce557 to 5f81fc4 Compare June 23, 2026 16:51
@declan-scale declan-scale force-pushed the declan-scale/init-template-openai-agents branch from 4310435 to 1638b8b Compare June 23, 2026 16:52
@declan-scale

Copy link
Copy Markdown
Contributor Author

@greptile review

@declan-scale declan-scale force-pushed the declan-scale/harness-integration-tests branch from 5f81fc4 to 7f0d754 Compare June 23, 2026 19:53
@declan-scale declan-scale force-pushed the declan-scale/init-template-openai-agents branch from 1638b8b to 3a13b08 Compare June 23, 2026 19:53
@declan-scale declan-scale force-pushed the declan-scale/harness-integration-tests branch from 7f0d754 to b158aa2 Compare June 23, 2026 19:57
@declan-scale declan-scale force-pushed the declan-scale/init-template-openai-agents branch from 3a13b08 to e4db8d5 Compare June 23, 2026 19:57

@danielmillerp danielmillerp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you I know this was missing haha

@declan-scale declan-scale force-pushed the declan-scale/init-template-openai-agents branch from e4db8d5 to 9fc5e23 Compare June 23, 2026 22:04
@declan-scale declan-scale force-pushed the declan-scale/harness-integration-tests branch from b158aa2 to 561884d Compare June 23, 2026 22:04
@declan-scale declan-scale force-pushed the declan-scale/init-template-openai-agents branch from 9fc5e23 to f55806c Compare June 23, 2026 22:29
@declan-scale declan-scale force-pushed the declan-scale/harness-integration-tests branch from 561884d to 96141e0 Compare June 23, 2026 22:29
Comment on lines +64 to +121
"# - ToolRequestContent: A message with a tool request, which contains a JSON-serializable request to call a tool\n",
"# - ToolResponseContent: A message with a tool response, which contains response object from a tool call in its content\n",
"\n",
"# When processing the message/send response, if you are expecting more than TextContent, such as DataContent, ToolRequestContent, or ToolResponseContent, you can process them as well\n",
"\n",
"rpc_response = client.agents.send_message(\n",
" agent_name=AGENT_NAME,\n",
" params={\n",
" \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n",
" \"stream\": False\n",
" }\n",
")\n",
"\n",
"if not rpc_response or not rpc_response.result:\n",
" raise ValueError(\"No result in response\")\n",
"\n",
"# Extract and print just the text content from the response\n",
"for task_message in rpc_response.result:\n",
" content = task_message.content\n",
" if isinstance(content, TextContent):\n",
" text = content.content\n",
" print(text)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "79688331",
"metadata": {},
"outputs": [],
"source": [
"# Test streaming response\n",
"from agentex.types.task_message_update import StreamTaskMessageDelta, StreamTaskMessageFull\n",
"from agentex.types.text_delta import TextDelta\n",
"\n",
"\n",
"# The result object of message/send will be a TaskMessageUpdate which is a union of the following types:\n",
"# - StreamTaskMessageStart: \n",
"# - An indicator that a streaming message was started, doesn't contain any useful content\n",
"# - StreamTaskMessageDelta: \n",
"# - A delta of a streaming message, contains the text delta to aggregate\n",
"# - StreamTaskMessageDone: \n",
"# - An indicator that a streaming message was done, doesn't contain any useful content\n",
"# - StreamTaskMessageFull: \n",
"# - A non-streaming message, there is nothing to aggregate, since this contains the full message, not deltas\n",
"\n",
"# Whenn processing StreamTaskMessageDelta, if you are expecting more than TextDeltas, such as DataDelta, ToolRequestDelta, or ToolResponseDelta, you can process them as well\n",
"# Whenn processing StreamTaskMessageFull, if you are expecting more than TextContent, such as DataContent, ToolRequestContent, or ToolResponseContent, you can process them as well\n",
"\n",
"for agent_rpc_response_chunk in client.agents.send_message_stream(\n",
" agent_name=AGENT_NAME,\n",
" params={\n",
" \"content\": {\"type\": \"text\", \"author\": \"user\", \"content\": \"Hello what can you do?\"},\n",
" \"stream\": True\n",
" }\n",
"):\n",
" # We know that the result of the message/send when stream is set to True will be a TaskMessageUpdate\n",
" task_message_update = agent_rpc_response_chunk.result\n",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Notebook calls wrong API

This notebook uses the sync message/send helpers, but the generated agent is an async ACP template that only registers event/send through on_task_event_send. The async FastACP server returns an immediate {status: "processing"} acknowledgement for async methods and does not expose a message/send handler, so both client.agents.send_message(...) and send_message_stream(...) can either get Method message/send not found or never receive the agent output. The async notebook should send an event and then subscribe to the task messages, like the base async template.

Artifacts

Repro: notebook-equivalent JSON-RPC harness

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Repro: failing message/send and accepted event/send output

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agentex/lib/cli/templates/default-openai-agents/dev.ipynb.j2
Line: 64-121

Comment:
**Notebook calls wrong API**

This notebook uses the sync `message/send` helpers, but the generated agent is an async ACP template that only registers `event/send` through `on_task_event_send`. The async FastACP server returns an immediate `{status: "processing"}` acknowledgement for async methods and does not expose a `message/send` handler, so both `client.agents.send_message(...)` and `send_message_stream(...)` can either get `Method message/send not found` or never receive the agent output. The async notebook should send an event and then subscribe to the task messages, like the base async template.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

@declan-scale declan-scale force-pushed the declan-scale/harness-integration-tests branch from 96141e0 to b30a90b Compare June 23, 2026 23:47
Base automatically changed from declan-scale/harness-integration-tests to next June 24, 2026 00:36
declan-scale and others added 2 commits June 23, 2026 20:38
Add the missing async-base OpenAI Agents SDK template, wiring
DEFAULT_OPENAI_AGENTS into the init flow (enum, project files, async prompt).
The scaffolded acp.py imports OpenAITurn from the agentex.lib.adk facade,
matching the other Turn-based templates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Greptile review: the async base handler ran each turn with only the
current user_message, so multi-turn context was dropped. Persist the OpenAI
input list per task via adk.state and replay it into Runner.run_streamed,
matching the sync and temporal openai-agents templates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@declan-scale declan-scale force-pushed the declan-scale/init-template-openai-agents branch from f55806c to 9861b8b Compare June 24, 2026 00:38
@declan-scale declan-scale merged commit 624e9c8 into next Jun 24, 2026
48 checks passed
@declan-scale declan-scale deleted the declan-scale/init-template-openai-agents branch June 24, 2026 00:46
@stainless-app stainless-app Bot mentioned this pull request Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants