From 4e9b7b21cbc445d65e58e5988df97874c437fbcf Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Tue, 23 Jun 2026 23:05:33 -0400 Subject: [PATCH 1/5] fix(cli): harden init templates per Greptile feedback (suite-wide) Addresses the systemic template issues Greptile flagged across the init template PRs (#434/#435/#436), applied consistently to every affected template instead of one family: - manifest.yaml.j2 (19): render `description` via `{{ description | tojson }}` so a user-supplied description containing YAML-significant characters (`:`, `#`, quotes) can no longer produce an invalid or truncated manifest. - Dockerfile-uv.j2 (19): `agentex init` renders `pyproject.toml` but no `uv.lock`, so `COPY ... uv.lock` + `uv sync --locked` broke a fresh uv build. Drop `uv.lock` from the COPY and `--locked` from `uv sync` so a freshly scaffolded project builds out of the box. - acp.py.j2 / workflow.py.j2 (non-text events): `params(.event).content` is a TaskMessageContent union; reading `.content` on a data/tool message raised AttributeError. Guard with `isinstance(content, TextContent)` before reading the text (async handlers return early, sync handlers return a TextContent notice, streaming handlers end the stream). - default-codex acp.py.j2 (concurrency): serialize turns per task with an asyncio lock so two near-simultaneous events can no longer both read a stale `codex_thread_id` and fork the session. (The temporal-codex variant already serializes via its own turn lock.) Verified by rendering every template: all project Python compiles and every manifest parses with a YAML-hostile description value. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../default-claude-code/Dockerfile-uv.j2 | 6 +- .../default-claude-code/manifest.yaml.j2 | 2 +- .../default-claude-code/project/acp.py.j2 | 7 +- .../templates/default-codex/Dockerfile-uv.j2 | 6 +- .../templates/default-codex/manifest.yaml.j2 | 2 +- .../templates/default-codex/project/acp.py.j2 | 161 ++++++++++-------- .../default-langgraph/Dockerfile-uv.j2 | 6 +- .../default-langgraph/manifest.yaml.j2 | 2 +- .../default-langgraph/project/acp.py.j2 | 7 +- .../default-openai-agents/Dockerfile-uv.j2 | 6 +- .../default-openai-agents/manifest.yaml.j2 | 2 +- .../default-openai-agents/project/acp.py.j2 | 7 +- .../default-pydantic-ai/Dockerfile-uv.j2 | 6 +- .../default-pydantic-ai/manifest.yaml.j2 | 2 +- .../default-pydantic-ai/project/acp.py.j2 | 7 +- .../cli/templates/default/Dockerfile-uv.j2 | 6 +- .../cli/templates/default/manifest.yaml.j2 | 2 +- .../sync-claude-code/Dockerfile-uv.j2 | 6 +- .../sync-claude-code/manifest.yaml.j2 | 2 +- .../sync-claude-code/project/acp.py.j2 | 7 +- .../cli/templates/sync-codex/Dockerfile-uv.j2 | 6 +- .../cli/templates/sync-codex/manifest.yaml.j2 | 2 +- .../templates/sync-codex/project/acp.py.j2 | 7 +- .../templates/sync-langgraph/Dockerfile-uv.j2 | 6 +- .../templates/sync-langgraph/manifest.yaml.j2 | 2 +- .../sync-langgraph/project/acp.py.j2 | 7 +- .../Dockerfile-uv.j2 | 6 +- .../manifest.yaml.j2 | 2 +- .../project/acp.py.j2 | 6 +- .../sync-openai-agents/Dockerfile-uv.j2 | 6 +- .../sync-openai-agents/manifest.yaml.j2 | 2 +- .../sync-openai-agents/project/acp.py.j2 | 7 +- .../sync-pydantic-ai/Dockerfile-uv.j2 | 6 +- .../sync-pydantic-ai/manifest.yaml.j2 | 2 +- .../sync-pydantic-ai/project/acp.py.j2 | 8 +- .../lib/cli/templates/sync/Dockerfile-uv.j2 | 6 +- .../lib/cli/templates/sync/manifest.yaml.j2 | 2 +- .../lib/cli/templates/sync/project/acp.py.j2 | 8 +- .../temporal-claude-code/Dockerfile-uv.j2 | 6 +- .../temporal-claude-code/manifest.yaml.j2 | 2 +- .../project/workflow.py.j2 | 6 +- .../templates/temporal-codex/Dockerfile-uv.j2 | 6 +- .../templates/temporal-codex/manifest.yaml.j2 | 2 +- .../temporal-codex/project/workflow.py.j2 | 7 +- .../temporal-langgraph/Dockerfile-uv.j2 | 6 +- .../temporal-langgraph/manifest.yaml.j2 | 2 +- .../temporal-langgraph/project/workflow.py.j2 | 6 +- .../temporal-openai-agents/Dockerfile-uv.j2 | 6 +- .../temporal-openai-agents/manifest.yaml.j2 | 2 +- .../project/workflow.py.j2 | 7 +- .../temporal-pydantic-ai/Dockerfile-uv.j2 | 6 +- .../temporal-pydantic-ai/manifest.yaml.j2 | 2 +- .../project/workflow.py.j2 | 11 +- .../cli/templates/temporal/Dockerfile-uv.j2 | 6 +- .../cli/templates/temporal/manifest.yaml.j2 | 2 +- 55 files changed, 268 insertions(+), 160 deletions(-) diff --git a/src/agentex/lib/cli/templates/default-claude-code/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/default-claude-code/Dockerfile-uv.j2 index 461e55c33..93d0f82d1 100644 --- a/src/agentex/lib/cli/templates/default-claude-code/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/default-claude-code/Dockerfile-uv.j2 @@ -31,18 +31,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/default-claude-code/manifest.yaml.j2 b/src/agentex/lib/cli/templates/default-claude-code/manifest.yaml.j2 index f8217edf9..ee08bc91b 100644 --- a/src/agentex/lib/cli/templates/default-claude-code/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/default-claude-code/manifest.yaml.j2 @@ -65,7 +65,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 index 85f98322a..42512c601 100644 --- a/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-claude-code/project/acp.py.j2 @@ -27,6 +27,7 @@ from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.fastacp import AsyncACPConfig from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config @@ -134,7 +135,11 @@ async def handle_task_create(params: CreateTaskParams): async def handle_task_event_send(params: SendEventParams): """Handle a user message: spawn Claude Code locally and push events to the task stream.""" task_id = params.task.id - prompt = params.event.content.content + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + prompt = content.content logger.info("Processing message for task %s", task_id) await adk.messages.create(task_id=task_id, content=params.event.content) diff --git a/src/agentex/lib/cli/templates/default-codex/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/default-codex/Dockerfile-uv.j2 index dffe96519..02860b9b9 100644 --- a/src/agentex/lib/cli/templates/default-codex/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/default-codex/Dockerfile-uv.j2 @@ -31,18 +31,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/default-codex/manifest.yaml.j2 b/src/agentex/lib/cli/templates/default-codex/manifest.yaml.j2 index aef2fcb5f..3c894318f 100644 --- a/src/agentex/lib/cli/templates/default-codex/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/default-codex/manifest.yaml.j2 @@ -65,7 +65,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 index f3fd91104..e052dd65b 100644 --- a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 @@ -32,6 +32,7 @@ load_dotenv() import agentex.lib.adk as adk from agentex.lib.adk import CodexTurn from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams +from agentex.types.text_content import TextContent from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.fastacp import AsyncACPConfig from agentex.lib.types.tracing import SGPTracingProcessorConfig @@ -57,6 +58,20 @@ acp = FastACP.create( MODEL = os.environ.get("CODEX_MODEL", "o4-mini") +# Serialize turns per task. Two ``task/event/send`` calls for the same task can +# otherwise both read the old ``codex_thread_id`` (or ``None``), run independent +# codex turns, and race to overwrite the stored thread id — forking the session. +# A per-task lock keeps turns sequential without blocking other tasks. +_task_locks: dict[str, asyncio.Lock] = {} + + +def _task_lock(task_id: str) -> asyncio.Lock: + lock = _task_locks.get(task_id) + if lock is None: + lock = asyncio.Lock() + _task_locks[task_id] = lock + return lock + class ConversationState(BaseModel): """Per-task conversation state persisted via ``adk.state``. @@ -150,81 +165,93 @@ async def handle_task_event_send(params: SendEventParams): """Handle each user message: spawn codex, stream events, save thread ID.""" task_id = params.task.id agent_id = params.agent.id - user_message = params.event.content.content - - logger.info("Processing message for task %s", task_id) - - await adk.messages.create(task_id=task_id, content=params.event.content) - - task_state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id) - if task_state is None: - state = ConversationState() - task_state = await adk.state.create(task_id=task_id, agent_id=agent_id, state=state) - else: - state = ConversationState.model_validate(task_state.state) - state.turn_number += 1 + content = params.event.content + if not isinstance(content, TextContent): + logger.warning( + "Ignoring non-text event content (type=%s) for task %s", + getattr(content, "type", "?"), + task_id, + ) + return + user_message = content.content - async with adk.tracing.span( - trace_id=task_id, - task_id=task_id, - name=f"Turn {state.turn_number}", - input={"message": user_message}, - data={"__span_type__": "AGENT_WORKFLOW"}, - ) as turn_span: - start_ms = int(time.monotonic() * 1000) + logger.info("Processing message for task %s", task_id) - process = await _spawn_codex(MODEL, thread_id=state.codex_thread_id) + await adk.messages.create(task_id=task_id, content=content) - assert process.stdin is not None - process.stdin.write(user_message.encode("utf-8")) - await process.stdin.drain() - process.stdin.close() + # Serialize the read-modify-write of ``codex_thread_id`` so two concurrent + # turns on the same task cannot fork the codex session. + async with _task_lock(task_id): + task_state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id) + if task_state is None: + state = ConversationState() + task_state = await adk.state.create(task_id=task_id, agent_id=agent_id, state=state) + else: + state = ConversationState.model_validate(task_state.state) - turn = CodexTurn( - events=_process_stdout(process), - model=MODEL, - ) + state.turn_number += 1 - emitter = UnifiedEmitter( - task_id=task_id, + async with adk.tracing.span( trace_id=task_id, - parent_span_id=turn_span.id if turn_span else None, - ) - - # Guarantee the subprocess is reaped even if auto_send_turn raises - # (e.g. a Redis error); otherwise codex stays blocked writing to a full - # stdout pipe buffer and the OS process leaks until the server restarts. - try: - result = await emitter.auto_send_turn(turn) - finally: - if process.returncode is None: - process.kill() - await process.wait() - - # Record the real wall-clock duration AFTER streaming completes; setting - # it before the stream ran would capture only subprocess spawn overhead. - turn.duration_ms = int(time.monotonic() * 1000) - start_ms - - usage = turn.usage() - - # Persist the codex session id (public accessor; valid post-stream) so the - # next turn resumes the same session. - if turn.session_id: - state.codex_thread_id = turn.session_id - - await adk.state.update( - state_id=task_state.id, task_id=task_id, - agent_id=agent_id, - state=state, - ) - - if turn_span: - turn_span.output = { - "final_text": result.final_text, - "model": usage.model, - } + name=f"Turn {state.turn_number}", + input={"message": user_message}, + data={"__span_type__": "AGENT_WORKFLOW"}, + ) as turn_span: + start_ms = int(time.monotonic() * 1000) + + process = await _spawn_codex(MODEL, thread_id=state.codex_thread_id) + + assert process.stdin is not None + process.stdin.write(user_message.encode("utf-8")) + await process.stdin.drain() + process.stdin.close() + + turn = CodexTurn( + events=_process_stdout(process), + model=MODEL, + ) + + emitter = UnifiedEmitter( + task_id=task_id, + trace_id=task_id, + parent_span_id=turn_span.id if turn_span else None, + ) + + # Guarantee the subprocess is reaped even if auto_send_turn raises + # (e.g. a Redis error); otherwise codex stays blocked writing to a full + # stdout pipe buffer and the OS process leaks until the server restarts. + try: + result = await emitter.auto_send_turn(turn) + finally: + if process.returncode is None: + process.kill() + await process.wait() + + # Record the real wall-clock duration AFTER streaming completes; setting + # it before the stream ran would capture only subprocess spawn overhead. + turn.duration_ms = int(time.monotonic() * 1000) - start_ms + + usage = turn.usage() + + # Persist the codex session id (public accessor; valid post-stream) so the + # next turn resumes the same session. + if turn.session_id: + state.codex_thread_id = turn.session_id + + await adk.state.update( + state_id=task_state.id, + task_id=task_id, + agent_id=agent_id, + state=state, + ) + + if turn_span: + turn_span.output = { + "final_text": result.final_text, + "model": usage.model, + } @acp.on_task_cancel diff --git a/src/agentex/lib/cli/templates/default-langgraph/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/default-langgraph/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/default-langgraph/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/default-langgraph/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/default-langgraph/manifest.yaml.j2 b/src/agentex/lib/cli/templates/default-langgraph/manifest.yaml.j2 index 2d94ba41c..e6c15cf33 100644 --- a/src/agentex/lib/cli/templates/default-langgraph/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/default-langgraph/manifest.yaml.j2 @@ -65,7 +65,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 index 750a271ad..da5d37905 100644 --- a/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-langgraph/project/acp.py.j2 @@ -22,6 +22,7 @@ from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskPa from agentex.lib.types.fastacp import AsyncACPConfig from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent from agentex.lib.adk import LangGraphTurn from project.graph import create_graph @@ -55,7 +56,11 @@ async def handle_task_event_send(params: SendEventParams): """Handle incoming events, streaming tokens and tool calls via Redis.""" graph = await get_graph() task_id = params.task.id - user_message = params.event.content.content + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + user_message = content.content logger.info(f"Processing message for thread {task_id}") diff --git a/src/agentex/lib/cli/templates/default-openai-agents/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/default-openai-agents/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/default-openai-agents/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/default-openai-agents/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/default-openai-agents/manifest.yaml.j2 b/src/agentex/lib/cli/templates/default-openai-agents/manifest.yaml.j2 index deae08dee..b633518be 100644 --- a/src/agentex/lib/cli/templates/default-openai-agents/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/default-openai-agents/manifest.yaml.j2 @@ -64,7 +64,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 index fd7d7c4c6..66ee31243 100644 --- a/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-openai-agents/project/acp.py.j2 @@ -27,6 +27,7 @@ from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskP from agentex.lib.types.fastacp import AsyncACPConfig from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent from agentex.lib.utils.model_utils import BaseModel from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.lib.core.harness.emitter import UnifiedEmitter @@ -112,7 +113,11 @@ async def handle_task_event_send(params: SendEventParams): agent = get_agent() task_id = params.task.id agent_id = params.agent.id - user_message = params.event.content.content + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + user_message = content.content logger.info(f"Processing message for task {task_id}") diff --git a/src/agentex/lib/cli/templates/default-pydantic-ai/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/default-pydantic-ai/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/default-pydantic-ai/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/default-pydantic-ai/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/default-pydantic-ai/manifest.yaml.j2 b/src/agentex/lib/cli/templates/default-pydantic-ai/manifest.yaml.j2 index 2d94ba41c..e6c15cf33 100644 --- a/src/agentex/lib/cli/templates/default-pydantic-ai/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/default-pydantic-ai/manifest.yaml.j2 @@ -65,7 +65,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 index 11d3ab476..245f9ec38 100644 --- a/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-pydantic-ai/project/acp.py.j2 @@ -29,6 +29,7 @@ from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.fastacp import AsyncACPConfig from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent from agentex.lib.utils.model_utils import BaseModel from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.lib.adk import PydanticAITurn @@ -97,7 +98,11 @@ async def handle_task_event_send(params: SendEventParams): agent = get_agent() task_id = params.task.id agent_id = params.agent.id - user_message = params.event.content.content + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + user_message = content.content logger.info(f"Processing message for task {task_id}") diff --git a/src/agentex/lib/cli/templates/default/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/default/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/default/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/default/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/default/manifest.yaml.j2 b/src/agentex/lib/cli/templates/default/manifest.yaml.j2 index 61c9064ed..c78ce1f44 100644 --- a/src/agentex/lib/cli/templates/default/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/default/manifest.yaml.j2 @@ -65,7 +65,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/sync-claude-code/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/sync-claude-code/Dockerfile-uv.j2 index 461e55c33..93d0f82d1 100644 --- a/src/agentex/lib/cli/templates/sync-claude-code/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/sync-claude-code/Dockerfile-uv.j2 @@ -31,18 +31,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/sync-claude-code/manifest.yaml.j2 b/src/agentex/lib/cli/templates/sync-claude-code/manifest.yaml.j2 index 429696a14..4432d1a33 100644 --- a/src/agentex/lib/cli/templates/sync-claude-code/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/sync-claude-code/manifest.yaml.j2 @@ -64,7 +64,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 index c739a188b..33a89a51e 100644 --- a/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-claude-code/project/acp.py.j2 @@ -27,6 +27,7 @@ from agentex.lib.types.acp import SendMessageParams from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.types.task_message_update import TaskMessageUpdate from agentex.types.task_message_content import TaskMessageContent @@ -130,7 +131,11 @@ async def handle_message_send( ) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]: """Handle an incoming message: run Claude Code locally and stream events.""" task_id = params.task.id - prompt = params.content.content + content = params.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text message content (type=%s)", getattr(content, "type", "?")) + return + prompt = content.content logger.info("Processing message for task %s", task_id) async with adk.tracing.span( diff --git a/src/agentex/lib/cli/templates/sync-codex/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/sync-codex/Dockerfile-uv.j2 index dffe96519..02860b9b9 100644 --- a/src/agentex/lib/cli/templates/sync-codex/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/sync-codex/Dockerfile-uv.j2 @@ -31,18 +31,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/sync-codex/manifest.yaml.j2 b/src/agentex/lib/cli/templates/sync-codex/manifest.yaml.j2 index 8810f6175..4e3cc0c3a 100644 --- a/src/agentex/lib/cli/templates/sync-codex/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/sync-codex/manifest.yaml.j2 @@ -64,7 +64,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 index 721721d41..0bc5d66a7 100644 --- a/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2 @@ -36,6 +36,7 @@ from agentex.lib.types.acp import SendMessageParams from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.types.task_message_update import TaskMessageUpdate from agentex.types.task_message_content import TaskMessageContent @@ -125,7 +126,11 @@ async def handle_message_send( ) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]: """Handle each message by running ``codex exec`` locally and streaming events.""" task_id = params.task.id - user_message = params.content.content + content = params.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text message content (type=%s)", getattr(content, "type", "?")) + return + user_message = content.content logger.info("Processing message for task %s", task_id) start_ms = int(time.monotonic() * 1000) diff --git a/src/agentex/lib/cli/templates/sync-langgraph/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/sync-langgraph/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/sync-langgraph/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/sync-langgraph/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/sync-langgraph/manifest.yaml.j2 b/src/agentex/lib/cli/templates/sync-langgraph/manifest.yaml.j2 index 7bf2cb355..33f2d7b67 100644 --- a/src/agentex/lib/cli/templates/sync-langgraph/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/sync-langgraph/manifest.yaml.j2 @@ -64,7 +64,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 index c6814b9c4..32d261093 100644 --- a/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2 @@ -14,6 +14,7 @@ from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.protocol.acp import SendMessageParams from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent from agentex.lib.adk import LangGraphTurn from agentex.types.task_message_content import TaskMessageContent from agentex.types.task_message_delta import TextDelta @@ -63,7 +64,11 @@ async def handle_message_send( graph = await get_graph() thread_id = params.task.id - user_message = params.content.content + content = params.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text message content (type=%s)", getattr(content, "type", "?")) + return + user_message = content.content logger.info(f"Processing message for thread {thread_id}") diff --git a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/manifest.yaml.j2 b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/manifest.yaml.j2 index bc2910f2a..6377d01cd 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/manifest.yaml.j2 @@ -64,7 +64,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 index e394e14c2..14af98351 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/project/acp.py.j2 @@ -63,7 +63,11 @@ async def handle_message_send( ) -> TaskMessageContent: """Handle incoming messages by running the local-sandbox agent.""" task_id = params.task.id - user_message = params.content.content + content = params.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text message content (type=%s)", getattr(content, "type", "?")) + return TextContent(author="agent", content="Sorry, I can only handle text messages right now.") + user_message = content.content logger.info(f"Processing message for task {task_id}") async with adk.tracing.span( diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 index 965769233..875fcc5e0 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents/manifest.yaml.j2 @@ -64,7 +64,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 index 4e2517838..41029f2ce 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents/project/acp.py.j2 @@ -98,7 +98,12 @@ async def handle_message_send( ) return - user_prompt = params.content.content + content = params.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text message content (type=%s)", getattr(content, "type", "?")) + return + + user_prompt = content.content # Retrieve the task state. Each event is handled as a new turn, so we need to get the state for the current turn. task_state = await adk.state.get_by_task_and_agent(task_id=params.task.id, agent_id=params.agent.id) diff --git a/src/agentex/lib/cli/templates/sync-pydantic-ai/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/sync-pydantic-ai/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/sync-pydantic-ai/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/sync-pydantic-ai/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/sync-pydantic-ai/manifest.yaml.j2 b/src/agentex/lib/cli/templates/sync-pydantic-ai/manifest.yaml.j2 index 965769233..875fcc5e0 100644 --- a/src/agentex/lib/cli/templates/sync-pydantic-ai/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/sync-pydantic-ai/manifest.yaml.j2 @@ -64,7 +64,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 index 061ae0e08..1a3c6f0a9 100644 --- a/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync-pydantic-ai/project/acp.py.j2 @@ -22,6 +22,7 @@ from agentex.protocol.acp import SendMessageParams from agentex.lib.core.harness import UnifiedEmitter from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger +from agentex.types.text_content import TextContent from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.types.task_message_update import TaskMessageUpdate from agentex.types.task_message_content import TaskMessageContent @@ -67,7 +68,12 @@ async def handle_message_send( agent = get_agent() task_id = params.task.id - user_message = params.content.content + content = params.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text message content (type=%s)", getattr(content, "type", "?")) + return + + user_message = content.content logger.info(f"Processing message for task {task_id}") # Open a per-message turn span. Tool calls below nest underneath this diff --git a/src/agentex/lib/cli/templates/sync/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/sync/Dockerfile-uv.j2 index 582434ac9..dd3035f7b 100644 --- a/src/agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/sync/Dockerfile-uv.j2 @@ -27,18 +27,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" ENV PYTHONPATH=/app diff --git a/src/agentex/lib/cli/templates/sync/manifest.yaml.j2 b/src/agentex/lib/cli/templates/sync/manifest.yaml.j2 index 965769233..875fcc5e0 100644 --- a/src/agentex/lib/cli/templates/sync/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/sync/manifest.yaml.j2 @@ -64,7 +64,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # Set enabled: true to use Temporal workflows for long-running tasks diff --git a/src/agentex/lib/cli/templates/sync/project/acp.py.j2 b/src/agentex/lib/cli/templates/sync/project/acp.py.j2 index ce5069a4c..d7d6f51d2 100644 --- a/src/agentex/lib/cli/templates/sync/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync/project/acp.py.j2 @@ -20,7 +20,13 @@ async def handle_message_send( params: SendMessageParams ) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]: """Default message handler with streaming support""" + content = params.content + if not isinstance(content, TextContent): + return TextContent( + author="agent", + content="Sorry, I can only handle text messages right now.", + ) return TextContent( author="agent", - content=f"Hello! I've received your message. Here's a generic response, but in future tutorials we'll see how you can get me to intelligently respond to your message. This is what I heard you say: {params.content.content}", + content=f"Hello! I've received your message. Here's a generic response, but in future tutorials we'll see how you can get me to intelligently respond to your message. This is what I heard you say: {content.content}", ) \ No newline at end of file diff --git a/src/agentex/lib/cli/templates/temporal-claude-code/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/temporal-claude-code/Dockerfile-uv.j2 index eb93d0aeb..f8746c573 100644 --- a/src/agentex/lib/cli/templates/temporal-claude-code/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/temporal-claude-code/Dockerfile-uv.j2 @@ -39,18 +39,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" diff --git a/src/agentex/lib/cli/templates/temporal-claude-code/manifest.yaml.j2 b/src/agentex/lib/cli/templates/temporal-claude-code/manifest.yaml.j2 index 0842d7fa3..9aa2b2b2f 100644 --- a/src/agentex/lib/cli/templates/temporal-claude-code/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/temporal-claude-code/manifest.yaml.j2 @@ -73,7 +73,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: "{{ description }}" + description: {{ description | tojson }} # Temporal workflow configuration # This enables your agent to run as a Temporal workflow for long-running tasks diff --git a/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 index e0c3a46e5..5837af7ed 100644 --- a/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 @@ -82,7 +82,11 @@ class {{ workflow_class }}(BaseWorkflow): async with self._turn_lock: self._turn_number += 1 task_id = params.task.id - prompt = params.event.content.content + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + prompt = content.content logger.info("Turn %d for task %s", self._turn_number, task_id) await adk.messages.create(task_id=task_id, content=params.event.content) diff --git a/src/agentex/lib/cli/templates/temporal-codex/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/temporal-codex/Dockerfile-uv.j2 index c72c7144c..7e31387fa 100644 --- a/src/agentex/lib/cli/templates/temporal-codex/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/temporal-codex/Dockerfile-uv.j2 @@ -39,18 +39,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" diff --git a/src/agentex/lib/cli/templates/temporal-codex/manifest.yaml.j2 b/src/agentex/lib/cli/templates/temporal-codex/manifest.yaml.j2 index d2a3df5c1..067567059 100644 --- a/src/agentex/lib/cli/templates/temporal-codex/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/temporal-codex/manifest.yaml.j2 @@ -73,7 +73,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: "{{ description }}" + description: {{ description | tojson }} # Temporal workflow configuration # This enables your agent to run as a Temporal workflow for long-running tasks diff --git a/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 index 39325ed60..d650234a0 100644 --- a/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 @@ -88,7 +88,12 @@ class {{ workflow_class }}(BaseWorkflow): await adk.messages.create(task_id=params.task.id, content=params.event.content) - user_message = params.event.content.content + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + + user_message = content.content async with adk.tracing.span( trace_id=params.task.id, diff --git a/src/agentex/lib/cli/templates/temporal-langgraph/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/temporal-langgraph/Dockerfile-uv.j2 index 2a3f1108b..6746869df 100644 --- a/src/agentex/lib/cli/templates/temporal-langgraph/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/temporal-langgraph/Dockerfile-uv.j2 @@ -33,18 +33,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" diff --git a/src/agentex/lib/cli/templates/temporal-langgraph/manifest.yaml.j2 b/src/agentex/lib/cli/templates/temporal-langgraph/manifest.yaml.j2 index 18cffd54a..b9216929f 100644 --- a/src/agentex/lib/cli/templates/temporal-langgraph/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/temporal-langgraph/manifest.yaml.j2 @@ -73,7 +73,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: "{{ description }}" + description: {{ description | tojson }} # Temporal workflow configuration # This enables your agent to run as a Temporal workflow for long-running tasks diff --git a/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 index d1621fb8c..96fe1eb7f 100644 --- a/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 @@ -94,7 +94,11 @@ class {{ workflow_class }}(BaseWorkflow): """Handle a new user message: echo it, then run the agent graph durably.""" logger.info(f"Received task event for task {params.task.id}") self._turn_number += 1 - user_text = params.event.content.content + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + user_text = content.content # Echo the user's message so it shows up as a chat bubble. await adk.messages.create(task_id=params.task.id, content=params.event.content) diff --git a/src/agentex/lib/cli/templates/temporal-openai-agents/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/temporal-openai-agents/Dockerfile-uv.j2 index 625592d31..0d9801016 100644 --- a/src/agentex/lib/cli/templates/temporal-openai-agents/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/temporal-openai-agents/Dockerfile-uv.j2 @@ -33,18 +33,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" diff --git a/src/agentex/lib/cli/templates/temporal-openai-agents/manifest.yaml.j2 b/src/agentex/lib/cli/templates/temporal-openai-agents/manifest.yaml.j2 index ee5e473d2..b9216929f 100644 --- a/src/agentex/lib/cli/templates/temporal-openai-agents/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/temporal-openai-agents/manifest.yaml.j2 @@ -73,7 +73,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # This enables your agent to run as a Temporal workflow for long-running tasks diff --git a/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 index 2b81bb335..af8b7a299 100644 --- a/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-openai-agents/project/workflow.py.j2 @@ -100,6 +100,11 @@ class {{ workflow_class }}(BaseWorkflow): async def on_task_event_send(self, params: SendEventParams) -> None: logger.info(f"Received task message instruction: {params}") + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + # Increment turn number for tracing self._state.turn_number += 1 @@ -108,7 +113,7 @@ class {{ workflow_class }}(BaseWorkflow): self._parent_span_id = params.task.id # Add the user message to conversation history - self._state.input_list.append({"role": "user", "content": params.event.content.content}) + self._state.input_list.append({"role": "user", "content": content.content}) # Echo back the client's message to show it in the UI await adk.messages.create(task_id=params.task.id, content=params.event.content) diff --git a/src/agentex/lib/cli/templates/temporal-pydantic-ai/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/temporal-pydantic-ai/Dockerfile-uv.j2 index 625592d31..0d9801016 100644 --- a/src/agentex/lib/cli/templates/temporal-pydantic-ai/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/temporal-pydantic-ai/Dockerfile-uv.j2 @@ -33,18 +33,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" diff --git a/src/agentex/lib/cli/templates/temporal-pydantic-ai/manifest.yaml.j2 b/src/agentex/lib/cli/templates/temporal-pydantic-ai/manifest.yaml.j2 index ee5e473d2..b9216929f 100644 --- a/src/agentex/lib/cli/templates/temporal-pydantic-ai/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/temporal-pydantic-ai/manifest.yaml.j2 @@ -73,7 +73,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # This enables your agent to run as a Temporal workflow for long-running tasks diff --git a/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 index 66a91d7a8..6dcca3002 100644 --- a/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-pydantic-ai/project/workflow.py.j2 @@ -85,6 +85,13 @@ class {{ workflow_class }}(BaseWorkflow): async def on_task_event_send(self, params: SendEventParams) -> None: """Handle a new user message: echo it, then run the agent durably.""" logger.info(f"Received task event: {params.task.id}") + + content = params.event.content + if not isinstance(content, TextContent): + logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) + return + user_message = content.content + self._turn_number += 1 # Echo the user's message so it shows up in the UI as a chat bubble. @@ -94,7 +101,7 @@ class {{ workflow_class }}(BaseWorkflow): trace_id=params.task.id, task_id=params.task.id, name=f"Turn {self._turn_number}", - input={"message": params.event.content.content}, + input={"message": user_message}, ) as span: # temporal_agent.run() is the magic line. Internally it schedules # a model activity (LLM HTTP call) and, for each tool the model @@ -107,7 +114,7 @@ class {{ workflow_class }}(BaseWorkflow): # without it the agent would respond to each user message as if # it had never seen the conversation before. result = await temporal_agent.run( - params.event.content.content, + user_message, message_history=self._message_history, deps=TaskDeps( task_id=params.task.id, diff --git a/src/agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 b/src/agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 index 625592d31..0d9801016 100644 --- a/src/agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +++ b/src/agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 @@ -33,18 +33,18 @@ ENV UV_HTTP_TIMEOUT=1000 WORKDIR /app/{{ project_path_from_build_root }} # Copy dependency files for layer caching -COPY {{ project_path_from_build_root }}/pyproject.toml {{ project_path_from_build_root }}/uv.lock ./ +COPY {{ project_path_from_build_root }}/pyproject.toml ./ # Install dependencies (without project itself, for layer caching) RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-install-project --no-dev + uv sync --no-install-project --no-dev # Copy the project code COPY {{ project_path_from_build_root }}/project ./project # Install the project RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync --locked --no-dev + uv sync --no-dev ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH" diff --git a/src/agentex/lib/cli/templates/temporal/manifest.yaml.j2 b/src/agentex/lib/cli/templates/temporal/manifest.yaml.j2 index ee5e473d2..b9216929f 100644 --- a/src/agentex/lib/cli/templates/temporal/manifest.yaml.j2 +++ b/src/agentex/lib/cli/templates/temporal/manifest.yaml.j2 @@ -73,7 +73,7 @@ agent: # Description of what your agent does # Helps with documentation and discovery - description: {{ description }} + description: {{ description | tojson }} # Temporal workflow configuration # This enables your agent to run as a Temporal workflow for long-running tasks From 14976f2ec4ef4faec053026f14c84257308a86e8 Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Tue, 23 Jun 2026 23:09:00 -0400 Subject: [PATCH 2/5] docs(cli): mirror non-text content guard in sync README snippets Update the illustrative handler snippets in the sync READMEs to use the same isinstance(content, TextContent) guard as the generated code, so the docs don't teach the crashing params.content.content pattern. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../templates/sync-openai-agents-local-sandbox/README.md.j2 | 5 ++++- .../lib/cli/templates/sync-openai-agents/README.md.j2 | 5 ++++- src/agentex/lib/cli/templates/sync-pydantic-ai/README.md.j2 | 5 ++++- src/agentex/lib/cli/templates/sync/README.md.j2 | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/README.md.j2 b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/README.md.j2 index 9416f2477..c49f0f56f 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/README.md.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents-local-sandbox/README.md.j2 @@ -262,7 +262,10 @@ Add sophisticated response generation: @acp.on_message_send async def handle_message_send(params: SendMessageParams): # Analyze input - user_message = params.content.content + content = params.content + if not isinstance(content, TextContent): + return TextContent(author="agent", content="Sorry, I can only handle text messages right now.") + user_message = content.content # Generate response response = await generate_intelligent_response(user_message) diff --git a/src/agentex/lib/cli/templates/sync-openai-agents/README.md.j2 b/src/agentex/lib/cli/templates/sync-openai-agents/README.md.j2 index a8ad10799..7711969cd 100644 --- a/src/agentex/lib/cli/templates/sync-openai-agents/README.md.j2 +++ b/src/agentex/lib/cli/templates/sync-openai-agents/README.md.j2 @@ -251,7 +251,10 @@ Add sophisticated response generation: @acp.on_message_send async def handle_message_send(params: SendMessageParams): # Analyze input - user_message = params.content.content + content = params.content + if not isinstance(content, TextContent): + return TextContent(author="agent", content="Sorry, I can only handle text messages right now.") + user_message = content.content # Generate response response = await generate_intelligent_response(user_message) diff --git a/src/agentex/lib/cli/templates/sync-pydantic-ai/README.md.j2 b/src/agentex/lib/cli/templates/sync-pydantic-ai/README.md.j2 index a8ad10799..7711969cd 100644 --- a/src/agentex/lib/cli/templates/sync-pydantic-ai/README.md.j2 +++ b/src/agentex/lib/cli/templates/sync-pydantic-ai/README.md.j2 @@ -251,7 +251,10 @@ Add sophisticated response generation: @acp.on_message_send async def handle_message_send(params: SendMessageParams): # Analyze input - user_message = params.content.content + content = params.content + if not isinstance(content, TextContent): + return TextContent(author="agent", content="Sorry, I can only handle text messages right now.") + user_message = content.content # Generate response response = await generate_intelligent_response(user_message) diff --git a/src/agentex/lib/cli/templates/sync/README.md.j2 b/src/agentex/lib/cli/templates/sync/README.md.j2 index a8ad10799..7711969cd 100644 --- a/src/agentex/lib/cli/templates/sync/README.md.j2 +++ b/src/agentex/lib/cli/templates/sync/README.md.j2 @@ -251,7 +251,10 @@ Add sophisticated response generation: @acp.on_message_send async def handle_message_send(params: SendMessageParams): # Analyze input - user_message = params.content.content + content = params.content + if not isinstance(content, TextContent): + return TextContent(author="agent", content="Sorry, I can only handle text messages right now.") + user_message = content.content # Generate response response = await generate_intelligent_response(user_message) From 6648b53b1733aa2a3aa20a9f52cacd8510e33ff8 Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Tue, 23 Jun 2026 23:22:35 -0400 Subject: [PATCH 3/5] fix(cli): address Greptile review on template hardening - default-codex: evict the per-task turn lock on cancel so _task_locks does not grow unbounded for the process lifetime. - temporal-langgraph: increment _turn_number after the non-text guard so a skipped non-text event no longer desyncs the turn counter. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 | 3 +++ .../cli/templates/temporal-langgraph/project/workflow.py.j2 | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 index e052dd65b..f5ca0adc8 100644 --- a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 @@ -257,3 +257,6 @@ async def handle_task_event_send(params: SendEventParams): @acp.on_task_cancel async def handle_task_canceled(params: CancelTaskParams): logger.info("Task canceled: %s", params.task.id) + # Evict the per-task turn lock so it doesn't accumulate for the lifetime of + # the process (one Lock per task otherwise lives forever). + _task_locks.pop(params.task.id, None) diff --git a/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 index 96fe1eb7f..14bafabc1 100644 --- a/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 @@ -93,11 +93,11 @@ class {{ workflow_class }}(BaseWorkflow): async def on_task_event_send(self, params: SendEventParams) -> None: """Handle a new user message: echo it, then run the agent graph durably.""" logger.info(f"Received task event for task {params.task.id}") - self._turn_number += 1 content = params.event.content if not isinstance(content, TextContent): logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) return + self._turn_number += 1 user_text = content.content # Echo the user's message so it shows up as a chat bubble. From 889fc3bcb9332eaec858b506c9ec18b4311b8449 Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Tue, 23 Jun 2026 23:30:53 -0400 Subject: [PATCH 4/5] fix(cli): address Greptile re-review on template hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - default-codex: make per-task lock eviction safe — evict after the turn releases the lock only when it is unlocked and has no waiters, instead of popping on cancel (which could drop a lock a running turn still holds and let a concurrent turn fork the session). - temporal-claude-code, temporal-codex: increment the turn counter after the non-text guard so a skipped non-text event no longer consumes a turn number (matching the temporal-langgraph fix). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../cli/templates/default-codex/project/acp.py.j2 | 13 +++++++++---- .../temporal-claude-code/project/workflow.py.j2 | 2 +- .../templates/temporal-codex/project/workflow.py.j2 | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 index f5ca0adc8..0534819b8 100644 --- a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 @@ -182,7 +182,8 @@ async def handle_task_event_send(params: SendEventParams): # Serialize the read-modify-write of ``codex_thread_id`` so two concurrent # turns on the same task cannot fork the codex session. - async with _task_lock(task_id): + lock = _task_lock(task_id) + async with lock: task_state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id) if task_state is None: state = ConversationState() @@ -253,10 +254,14 @@ async def handle_task_event_send(params: SendEventParams): "model": usage.model, } + # Evict the lock once this turn has released it and no other turn holds or + # is waiting on it, so ``_task_locks`` stays bounded without breaking the + # serialization guarantee. There is no await between ``_task_lock()`` and + # acquiring it, so an unlocked, waiter-free lock has no in-flight user. + if not lock.locked() and not getattr(lock, "_waiters", None): + _task_locks.pop(task_id, None) + @acp.on_task_cancel async def handle_task_canceled(params: CancelTaskParams): logger.info("Task canceled: %s", params.task.id) - # Evict the per-task turn lock so it doesn't accumulate for the lifetime of - # the process (one Lock per task otherwise lives forever). - _task_locks.pop(params.task.id, None) diff --git a/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 index 5837af7ed..8191ad80f 100644 --- a/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-claude-code/project/workflow.py.j2 @@ -80,12 +80,12 @@ class {{ workflow_class }}(BaseWorkflow): async def on_task_event_send(self, params: SendEventParams) -> None: """Handle a user message: spawn Claude Code and push events to the task stream.""" async with self._turn_lock: - self._turn_number += 1 task_id = params.task.id content = params.event.content if not isinstance(content, TextContent): logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) return + self._turn_number += 1 prompt = content.content logger.info("Turn %d for task %s", self._turn_number, task_id) diff --git a/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 index d650234a0..1004ebfb8 100644 --- a/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal-codex/project/workflow.py.j2 @@ -84,15 +84,15 @@ class {{ workflow_class }}(BaseWorkflow): """Handle a new user message: spawn codex, stream events via UnifiedEmitter.""" logger.info("Received task event: %s", params.task.id) async with self._turn_lock: - self._turn_number += 1 - - await adk.messages.create(task_id=params.task.id, content=params.event.content) - content = params.event.content if not isinstance(content, TextContent): logger.warning("Ignoring non-text event content (type=%s)", getattr(content, "type", "?")) return + self._turn_number += 1 + + await adk.messages.create(task_id=params.task.id, content=params.event.content) + user_message = content.content async with adk.tracing.span( From 7f7e1eaf5cb2a8e216318c1af3f4d361adc36c76 Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Tue, 23 Jun 2026 23:39:00 -0400 Subject: [PATCH 5/5] fix(cli): make default-codex lock failure-safe and echo inside lock - Acquire the per-task lock with try/finally so the lock is released and evicted even if the codex turn raises (previously eviction was skipped on the error path, leaking the lock). - Echo the user message inside the lock so concurrent turns' echoes stay ordered with their turns. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../templates/default-codex/project/acp.py.j2 | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 index 0534819b8..f676ef137 100644 --- a/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default-codex/project/acp.py.j2 @@ -178,12 +178,15 @@ async def handle_task_event_send(params: SendEventParams): logger.info("Processing message for task %s", task_id) - await adk.messages.create(task_id=task_id, content=content) - - # Serialize the read-modify-write of ``codex_thread_id`` so two concurrent - # turns on the same task cannot fork the codex session. + # Serialize the whole turn (echo + the read-modify-write of + # ``codex_thread_id``) so two concurrent turns on the same task cannot fork + # the codex session or interleave their echoed messages. lock = _task_lock(task_id) - async with lock: + await lock.acquire() + try: + # Echo inside the lock so this turn's message stays ordered with it. + await adk.messages.create(task_id=task_id, content=content) + task_state = await adk.state.get_by_task_and_agent(task_id=task_id, agent_id=agent_id) if task_state is None: state = ConversationState() @@ -253,13 +256,14 @@ async def handle_task_event_send(params: SendEventParams): "final_text": result.final_text, "model": usage.model, } - - # Evict the lock once this turn has released it and no other turn holds or - # is waiting on it, so ``_task_locks`` stays bounded without breaking the - # serialization guarantee. There is no await between ``_task_lock()`` and - # acquiring it, so an unlocked, waiter-free lock has no in-flight user. - if not lock.locked() and not getattr(lock, "_waiters", None): - _task_locks.pop(task_id, None) + finally: + lock.release() + # Evict the lock once released and idle (unlocked, no waiters) so + # ``_task_locks`` stays bounded even if the turn raised. There is no + # await between ``_task_lock()`` and acquiring it, so an unlocked, + # waiter-free lock has no in-flight user. + if not lock.locked() and not getattr(lock, "_waiters", None): + _task_locks.pop(task_id, None) @acp.on_task_cancel