From 7590970ec98227d1c789a8803abdaf54e5678345 Mon Sep 17 00:00:00 2001 From: Max Parke Date: Tue, 26 May 2026 23:46:40 -0400 Subject: [PATCH 1/5] refactor(types): promote protocol types to agentex.protocol.* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the JSON-RPC envelope types and ACP method-param types out of the hand-authored ADK overlay (agentex.lib.types.*) into a new canonical location at agentex.protocol.*. Back-compat shims at the old paths re-export the same classes, so existing imports continue to work. Motivation: The existing layout puts protocol-shape types under agentex.lib.types. That means a REST-only consumer who just wants typed JSON-RPC envelopes (e.g. egp-api-backend, which today hand-rolls a ~600-line gateway constructing `{"jsonrpc": "2.0", "method": "task/create", ...}` dicts by hand) can't import the types without pulling in the full heavy ADK runtime (~31 deps including temporalio, fastapi, claude-agent-sdk, etc.). This is the first step toward letting that consumer drop hand-rolled dict literals in favor of typed CreateTaskParams / SendMessageParams / SendEventParams / JSONRPCRequest / JSONRPCResponse models, ahead of the forthcoming slim-package split (a separate PR). Scope: - Move src/agentex/lib/types/acp.py → src/agentex/protocol/acp.py - Move src/agentex/lib/types/json_rpc.py → src/agentex/protocol/json_rpc.py - json_rpc.py: switch from `agentex.lib.utils.model_utils.BaseModel` (which transitively imports pyyaml) to `pydantic.BaseModel` directly. These classes don't use any model_utils extensions (from_yaml, to_json, populate_by_name) so the swap is behavior-preserving. - Add re-export shims at the old paths so existing `from agentex.lib.types.{acp,json_rpc} import ...` imports continue to work. Identity check confirms the shimmed and canonical classes are the same objects. - Update all internal usages within agentex.lib.* to import from the canonical path (9 files in src/agentex/lib + 1 in tests/). Files moved use only pydantic + agentex.types.{Task,Agent,Event, TaskMessageContent} — all slim-safe deps. Other agentex.lib.types.* modules with heavier deps (fastacp pulls temporal, converters pulls openai-agents) are left in place. Verified locally: - ruff check . → All checks passed - Wheel build → ships both agentex/protocol/* and shims at agentex/lib/types/{acp,json_rpc}.py - Import test from a fresh editable install: both canonical and shim paths resolve to the same class objects (identity-checked) Zero install-time impact for existing consumers — same import paths keep working. Tracking: AGX1-292 (the slim/heavy split that motivates this refactor lands as a follow-up PR). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../services/temporal_task_service.py | 2 +- .../lib/core/temporal/workflows/workflow.py | 2 +- .../lib/sdk/fastacp/base/base_acp_server.py | 4 +- .../lib/sdk/fastacp/impl/async_base_acp.py | 2 +- src/agentex/lib/sdk/fastacp/impl/sync_acp.py | 2 +- .../lib/sdk/fastacp/impl/temporal_acp.py | 2 +- src/agentex/lib/sdk/fastacp/tests/conftest.py | 4 +- .../sdk/fastacp/tests/test_base_acp_server.py | 2 +- .../lib/sdk/fastacp/tests/test_integration.py | 2 +- src/agentex/lib/types/acp.py | 129 ++---------------- src/agentex/lib/types/json_rpc.py | 58 ++------ src/agentex/protocol/__init__.py | 16 +++ src/agentex/protocol/acp.py | 116 ++++++++++++++++ src/agentex/protocol/json_rpc.py | 51 +++++++ tests/test_header_forwarding.py | 2 +- 15 files changed, 217 insertions(+), 177 deletions(-) create mode 100644 src/agentex/protocol/__init__.py create mode 100644 src/agentex/protocol/acp.py create mode 100644 src/agentex/protocol/json_rpc.py diff --git a/src/agentex/lib/core/temporal/services/temporal_task_service.py b/src/agentex/lib/core/temporal/services/temporal_task_service.py index 81eb22389..9d66c0c1f 100644 --- a/src/agentex/lib/core/temporal/services/temporal_task_service.py +++ b/src/agentex/lib/core/temporal/services/temporal_task_service.py @@ -6,7 +6,7 @@ from agentex.types.task import Task from agentex.types.agent import Agent from agentex.types.event import Event -from agentex.lib.types.acp import SendEventParams, CreateTaskParams +from agentex.protocol.acp import SendEventParams, CreateTaskParams from agentex.lib.environment_variables import EnvironmentVariables from agentex.lib.core.clients.temporal.types import WorkflowState from agentex.lib.core.temporal.types.workflow import SignalName diff --git a/src/agentex/lib/core/temporal/workflows/workflow.py b/src/agentex/lib/core/temporal/workflows/workflow.py index 727f3ac85..3e4498162 100644 --- a/src/agentex/lib/core/temporal/workflows/workflow.py +++ b/src/agentex/lib/core/temporal/workflows/workflow.py @@ -2,7 +2,7 @@ from temporalio import workflow -from agentex.lib.types.acp import SendEventParams, CreateTaskParams +from agentex.protocol.acp import SendEventParams, CreateTaskParams from agentex.lib.utils.logging import make_logger from agentex.lib.core.temporal.types.workflow import SignalName diff --git a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py index c48816020..b0b1c3685 100644 --- a/src/agentex/lib/sdk/fastacp/base/base_acp_server.py +++ b/src/agentex/lib/sdk/fastacp/base/base_acp_server.py @@ -14,7 +14,7 @@ from starlette.types import Send, Scope, ASGIApp, Receive from fastapi.responses import StreamingResponse -from agentex.lib.types.acp import ( +from agentex.protocol.acp import ( RPC_SYNC_METHODS, PARAMS_MODEL_BY_METHOD, RPCMethod, @@ -24,7 +24,7 @@ SendMessageParams, ) from agentex.lib.utils.logging import make_logger, ctx_var_request_id -from agentex.lib.types.json_rpc import JSONRPCError, JSONRPCRequest, JSONRPCResponse +from agentex.protocol.json_rpc import JSONRPCError, JSONRPCRequest, JSONRPCResponse from agentex.lib.utils.model_utils import BaseModel from agentex.lib.utils.registration import register_agent diff --git a/src/agentex/lib/sdk/fastacp/impl/async_base_acp.py b/src/agentex/lib/sdk/fastacp/impl/async_base_acp.py index d2b8bac92..e9d20f150 100644 --- a/src/agentex/lib/sdk/fastacp/impl/async_base_acp.py +++ b/src/agentex/lib/sdk/fastacp/impl/async_base_acp.py @@ -1,7 +1,7 @@ from typing import Any from typing_extensions import override -from agentex.lib.types.acp import ( +from agentex.protocol.acp import ( SendEventParams, CancelTaskParams, CreateTaskParams, diff --git a/src/agentex/lib/sdk/fastacp/impl/sync_acp.py b/src/agentex/lib/sdk/fastacp/impl/sync_acp.py index 4898a9637..5ecad073e 100644 --- a/src/agentex/lib/sdk/fastacp/impl/sync_acp.py +++ b/src/agentex/lib/sdk/fastacp/impl/sync_acp.py @@ -3,7 +3,7 @@ from typing import Any, override from collections.abc import AsyncGenerator -from agentex.lib.types.acp import SendMessageParams +from agentex.protocol.acp import SendMessageParams from agentex.lib.utils.logging import make_logger from agentex.types.task_message_delta import TextDelta from agentex.types.task_message_update import ( diff --git a/src/agentex/lib/sdk/fastacp/impl/temporal_acp.py b/src/agentex/lib/sdk/fastacp/impl/temporal_acp.py index f64e16d72..54fe72e6c 100644 --- a/src/agentex/lib/sdk/fastacp/impl/temporal_acp.py +++ b/src/agentex/lib/sdk/fastacp/impl/temporal_acp.py @@ -6,7 +6,7 @@ from fastapi import FastAPI from temporalio.converter import PayloadCodec -from agentex.lib.types.acp import ( +from agentex.protocol.acp import ( SendEventParams, CancelTaskParams, CreateTaskParams, diff --git a/src/agentex/lib/sdk/fastacp/tests/conftest.py b/src/agentex/lib/sdk/fastacp/tests/conftest.py index 8941f16eb..59ecbfee3 100644 --- a/src/agentex/lib/sdk/fastacp/tests/conftest.py +++ b/src/agentex/lib/sdk/fastacp/tests/conftest.py @@ -13,12 +13,12 @@ from agentex.types.task import Task from agentex.types.agent import Agent -from agentex.lib.types.acp import ( +from agentex.protocol.acp import ( CancelTaskParams, CreateTaskParams, SendMessageParams, ) -from agentex.lib.types.json_rpc import JSONRPCRequest +from agentex.protocol.json_rpc import JSONRPCRequest from agentex.types.task_message import TaskMessageContent from agentex.types.task_message_content import TextContent from agentex.lib.sdk.fastacp.impl.sync_acp import SyncACP diff --git a/src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py b/src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py index 0816ac436..8a218187e 100644 --- a/src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +++ b/src/agentex/lib/sdk/fastacp/tests/test_base_acp_server.py @@ -5,7 +5,7 @@ import pytest from fastapi.testclient import TestClient -from agentex.lib.types.acp import ( +from agentex.protocol.acp import ( RPCMethod, SendEventParams, CancelTaskParams, diff --git a/src/agentex/lib/sdk/fastacp/tests/test_integration.py b/src/agentex/lib/sdk/fastacp/tests/test_integration.py index c6f310af1..c72d336e3 100644 --- a/src/agentex/lib/sdk/fastacp/tests/test_integration.py +++ b/src/agentex/lib/sdk/fastacp/tests/test_integration.py @@ -5,7 +5,7 @@ import httpx import pytest -from agentex.lib.types.acp import ( +from agentex.protocol.acp import ( RPCMethod, SendEventParams, CancelTaskParams, diff --git a/src/agentex/lib/types/acp.py b/src/agentex/lib/types/acp.py index d719b4fd5..272521ad2 100644 --- a/src/agentex/lib/types/acp.py +++ b/src/agentex/lib/types/acp.py @@ -1,116 +1,13 @@ -from __future__ import annotations - -from enum import Enum -from typing import Any - -from pydantic import Field, BaseModel - -from agentex.types.task import Task -from agentex.types.agent import Agent -from agentex.types.event import Event -from agentex.types.task_message_content import TaskMessageContent - - -class RPCMethod(str, Enum): - """Available JSON-RPC methods for agent communication.""" - - EVENT_SEND = "event/send" - MESSAGE_SEND = "message/send" - TASK_CANCEL = "task/cancel" - TASK_CREATE = "task/create" - - -class CreateTaskParams(BaseModel): - """Parameters for task/create method. - - Attributes: - agent: The agent that the task was sent to. - task: The task to be created. - params: The parameters for the task as inputted by the user. - request: Additional request context including headers forwarded to this agent. - """ - - agent: Agent = Field(..., description="The agent that the task was sent to") - task: Task = Field(..., description="The task to be created") - params: dict[str, Any] | None = Field( - None, - description="The parameters for the task as inputted by the user", - ) - request: dict[str, Any] | None = Field( - default=None, - description="Additional request context including headers forwarded to this agent", - ) - - -class SendMessageParams(BaseModel): - """Parameters for message/send method. - - Attributes: - agent: The agent that the message was sent to. - task: The task that the message was sent to. - content: The message that was sent to the agent. - stream: Whether to stream the message back to the agentex server from the agent. - request: Additional request context including headers forwarded to this agent. - """ - - agent: Agent = Field(..., description="The agent that the message was sent to") - task: Task = Field(..., description="The task that the message was sent to") - content: TaskMessageContent = Field( - ..., description="The message that was sent to the agent" - ) - stream: bool = Field( - False, - description="Whether to stream the message back to the agentex server from the agent", - ) - request: dict[str, Any] | None = Field( - default=None, - description="Additional request context including headers forwarded to this agent", - ) - - -class SendEventParams(BaseModel): - """Parameters for event/send method. - - Attributes: - agent: The agent that the event was sent to. - task: The task that the message was sent to. - event: The event that was sent to the agent. - request: Additional request context including headers forwarded to this agent. - """ - - agent: Agent = Field(..., description="The agent that the event was sent to") - task: Task = Field(..., description="The task that the message was sent to") - event: Event = Field(..., description="The event that was sent to the agent") - request: dict[str, Any] | None = Field( - default=None, - description="Additional request context including headers forwarded to this agent", - ) - - -class CancelTaskParams(BaseModel): - """Parameters for task/cancel method. - - Attributes: - agent: The agent that the task was sent to. - task: The task that was cancelled. - request: Additional request context including headers forwarded to this agent. - """ - - agent: Agent = Field(..., description="The agent that the task was sent to") - task: Task = Field(..., description="The task that was cancelled") - request: dict[str, Any] | None = Field( - default=None, - description="Additional request context including headers forwarded to this agent", - ) - - -RPC_SYNC_METHODS = [ - RPCMethod.MESSAGE_SEND, -] - -PARAMS_MODEL_BY_METHOD: dict[RPCMethod, type[BaseModel]] = { - RPCMethod.EVENT_SEND: SendEventParams, - RPCMethod.TASK_CANCEL: CancelTaskParams, - RPCMethod.MESSAGE_SEND: SendMessageParams, - RPCMethod.TASK_CREATE: CreateTaskParams, -} +"""Back-compat shim. The canonical location is :mod:`agentex.protocol.acp`. + +Kept here so existing ``from agentex.lib.types.acp import ...`` imports +continue to work. New code should import from the canonical path. +""" + +from agentex.protocol.acp import ( # noqa: F401 + RPCMethod, + SendEventParams, + CancelTaskParams, + CreateTaskParams, + SendMessageParams, +) diff --git a/src/agentex/lib/types/json_rpc.py b/src/agentex/lib/types/json_rpc.py index b89e9d6b2..b010f93f7 100644 --- a/src/agentex/lib/types/json_rpc.py +++ b/src/agentex/lib/types/json_rpc.py @@ -1,51 +1,11 @@ -from __future__ import annotations +"""Back-compat shim. The canonical location is :mod:`agentex.protocol.json_rpc`. -from typing import Any, Literal +Kept here so existing ``from agentex.lib.types.json_rpc import ...`` imports +continue to work. New code should import from the canonical path. +""" -from agentex.lib.utils.model_utils import BaseModel - - -class JSONRPCError(BaseModel): - """JSON-RPC 2.0 Error - - Attributes: - code: The error code - message: The error message - data: The error data - """ - - code: int - message: str - data: Any | None = None - - -class JSONRPCRequest(BaseModel): - """JSON-RPC 2.0 Request - - Attributes: - jsonrpc: The JSON-RPC version - method: The method to call - params: The parameters for the request - id: The ID of the request - """ - - jsonrpc: Literal["2.0"] = "2.0" - method: str - params: dict[str, Any] - id: int | str | None = None - - -class JSONRPCResponse(BaseModel): - """JSON-RPC 2.0 Response - - Attributes: - jsonrpc: The JSON-RPC version - result: The result of the request - error: The error of the request - id: The ID of the request - """ - - jsonrpc: Literal["2.0"] = "2.0" - result: dict[str, Any] | None = None - error: JSONRPCError | None = None - id: int | str | None = None +from agentex.protocol.json_rpc import ( # noqa: F401 + JSONRPCError, + JSONRPCRequest, + JSONRPCResponse, +) diff --git a/src/agentex/protocol/__init__.py b/src/agentex/protocol/__init__.py new file mode 100644 index 000000000..be9db981a --- /dev/null +++ b/src/agentex/protocol/__init__.py @@ -0,0 +1,16 @@ +"""Wire-protocol shapes for Agentex. + +The modules under `agentex.protocol.*` are the typed shapes for talking to +an Agentex agent over JSON-RPC (the ACP / Agent Communication Protocol) +without pulling in the heavy ADK runtime. They depend only on pydantic and +the Stainless-generated `agentex.types.*` surface, so they are safe to +import from a slim REST-only install. + +Hand-rolled JSON-RPC clients (e.g. the one in `egp-api-backend`) can switch +from constructing `{"jsonrpc": "2.0", "method": "...", "params": {...}}` +dicts by hand to constructing `JSONRPCRequest(method=RPCMethod.TASK_CREATE, +params=CreateTaskParams(...).model_dump())`. + +For back-compat, the same classes are re-exported from +`agentex.lib.types.{acp,json_rpc}` (the historical locations). +""" diff --git a/src/agentex/protocol/acp.py b/src/agentex/protocol/acp.py new file mode 100644 index 000000000..d719b4fd5 --- /dev/null +++ b/src/agentex/protocol/acp.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from enum import Enum +from typing import Any + +from pydantic import Field, BaseModel + +from agentex.types.task import Task +from agentex.types.agent import Agent +from agentex.types.event import Event +from agentex.types.task_message_content import TaskMessageContent + + +class RPCMethod(str, Enum): + """Available JSON-RPC methods for agent communication.""" + + EVENT_SEND = "event/send" + MESSAGE_SEND = "message/send" + TASK_CANCEL = "task/cancel" + TASK_CREATE = "task/create" + + +class CreateTaskParams(BaseModel): + """Parameters for task/create method. + + Attributes: + agent: The agent that the task was sent to. + task: The task to be created. + params: The parameters for the task as inputted by the user. + request: Additional request context including headers forwarded to this agent. + """ + + agent: Agent = Field(..., description="The agent that the task was sent to") + task: Task = Field(..., description="The task to be created") + params: dict[str, Any] | None = Field( + None, + description="The parameters for the task as inputted by the user", + ) + request: dict[str, Any] | None = Field( + default=None, + description="Additional request context including headers forwarded to this agent", + ) + + +class SendMessageParams(BaseModel): + """Parameters for message/send method. + + Attributes: + agent: The agent that the message was sent to. + task: The task that the message was sent to. + content: The message that was sent to the agent. + stream: Whether to stream the message back to the agentex server from the agent. + request: Additional request context including headers forwarded to this agent. + """ + + agent: Agent = Field(..., description="The agent that the message was sent to") + task: Task = Field(..., description="The task that the message was sent to") + content: TaskMessageContent = Field( + ..., description="The message that was sent to the agent" + ) + stream: bool = Field( + False, + description="Whether to stream the message back to the agentex server from the agent", + ) + request: dict[str, Any] | None = Field( + default=None, + description="Additional request context including headers forwarded to this agent", + ) + + +class SendEventParams(BaseModel): + """Parameters for event/send method. + + Attributes: + agent: The agent that the event was sent to. + task: The task that the message was sent to. + event: The event that was sent to the agent. + request: Additional request context including headers forwarded to this agent. + """ + + agent: Agent = Field(..., description="The agent that the event was sent to") + task: Task = Field(..., description="The task that the message was sent to") + event: Event = Field(..., description="The event that was sent to the agent") + request: dict[str, Any] | None = Field( + default=None, + description="Additional request context including headers forwarded to this agent", + ) + + +class CancelTaskParams(BaseModel): + """Parameters for task/cancel method. + + Attributes: + agent: The agent that the task was sent to. + task: The task that was cancelled. + request: Additional request context including headers forwarded to this agent. + """ + + agent: Agent = Field(..., description="The agent that the task was sent to") + task: Task = Field(..., description="The task that was cancelled") + request: dict[str, Any] | None = Field( + default=None, + description="Additional request context including headers forwarded to this agent", + ) + + +RPC_SYNC_METHODS = [ + RPCMethod.MESSAGE_SEND, +] + +PARAMS_MODEL_BY_METHOD: dict[RPCMethod, type[BaseModel]] = { + RPCMethod.EVENT_SEND: SendEventParams, + RPCMethod.TASK_CANCEL: CancelTaskParams, + RPCMethod.MESSAGE_SEND: SendMessageParams, + RPCMethod.TASK_CREATE: CreateTaskParams, +} diff --git a/src/agentex/protocol/json_rpc.py b/src/agentex/protocol/json_rpc.py new file mode 100644 index 000000000..658c937f2 --- /dev/null +++ b/src/agentex/protocol/json_rpc.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel + + +class JSONRPCError(BaseModel): + """JSON-RPC 2.0 Error + + Attributes: + code: The error code + message: The error message + data: The error data + """ + + code: int + message: str + data: Any | None = None + + +class JSONRPCRequest(BaseModel): + """JSON-RPC 2.0 Request + + Attributes: + jsonrpc: The JSON-RPC version + method: The method to call + params: The parameters for the request + id: The ID of the request + """ + + jsonrpc: Literal["2.0"] = "2.0" + method: str + params: dict[str, Any] + id: int | str | None = None + + +class JSONRPCResponse(BaseModel): + """JSON-RPC 2.0 Response + + Attributes: + jsonrpc: The JSON-RPC version + result: The result of the request + error: The error of the request + id: The ID of the request + """ + + jsonrpc: Literal["2.0"] = "2.0" + result: dict[str, Any] | None = None + error: JSONRPCError | None = None + id: int | str | None = None diff --git a/tests/test_header_forwarding.py b/tests/test_header_forwarding.py index 51c3a685f..596c6729c 100644 --- a/tests/test_header_forwarding.py +++ b/tests/test_header_forwarding.py @@ -46,7 +46,7 @@ class _StubTracer(_StubAsyncTracer): from agentex.lib.core.services.adk.acp.acp import ACPService from agentex.lib.sdk.fastacp.base.base_acp_server import BaseACPServer -from agentex.lib.types.acp import RPCMethod, SendMessageParams, SendEventParams +from agentex.protocol.acp import RPCMethod, SendMessageParams, SendEventParams from agentex.types.task_message_content import TextContent from agentex.lib.sdk.fastacp.impl.temporal_acp import TemporalACP from agentex.lib.core.temporal.services.temporal_task_service import TemporalTaskService From 348f6086cd5907cd4d956f53266e32751332bcc8 Mon Sep 17 00:00:00 2001 From: Max Parke Date: Wed, 27 May 2026 02:12:54 -0400 Subject: [PATCH 2/5] fix(types): address Greptile review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues from Greptile's review (4/5 confidence, all P1/P2): 1. Back-compat shim at `agentex.lib.types.acp` was missing two public names from the original module: `RPC_SYNC_METHODS` and `PARAMS_MODEL_BY_METHOD`. External consumers importing those from the old path would have hit ImportError, breaking the PR's "zero install- time impact" guarantee. Added both to the re-export list. 2. `agentex.protocol.json_rpc` silently dropped `from_attributes=True` and `populate_by_name=True` config when switching from `model_utils.BaseModel` to plain `pydantic.BaseModel`. Restored via an explicit `model_config = ConfigDict(...)` on all three classes, with a comment explaining why. The previous version inherited these flags transitively; making them explicit + documented avoids drift. 3. All 10 CLI scaffolding templates (`agentex init`) generated `from agentex.lib.types.acp import ...` — works via the shim but immediately stale on the day this PR establishes `agentex.protocol.acp` as canonical. Updated all templates to use the new path so scaffolded code starts on the right foot. Verified locally: - ruff check . → All checks passed - shim re-exports all 7 original symbols (5 classes + 2 module-level constants), identity-checked vs canonical - JSONRPCRequest/Response/Error all have `model_config.get('from_attributes') is True` and `populate_by_name is True` Co-Authored-By: Claude Opus 4.7 (1M context) --- .../templates/default-langgraph/project/acp.py.j2 | 2 +- .../default-pydantic-ai/project/acp.py.j2 | 2 +- .../lib/cli/templates/default/project/acp.py.j2 | 2 +- .../cli/templates/sync-langgraph/project/acp.py.j2 | 2 +- .../templates/sync-openai-agents/project/acp.py.j2 | 2 +- .../templates/sync-pydantic-ai/project/acp.py.j2 | 2 +- .../lib/cli/templates/sync/project/acp.py.j2 | 2 +- .../temporal-openai-agents/project/workflow.py.j2 | 2 +- .../temporal-pydantic-ai/project/workflow.py.j2 | 2 +- .../cli/templates/temporal/project/workflow.py.j2 | 2 +- src/agentex/lib/types/acp.py | 2 ++ src/agentex/protocol/json_rpc.py | 14 +++++++++++++- 12 files changed, 25 insertions(+), 11 deletions(-) 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 6099f5c3e..3309dc07e 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 @@ -18,7 +18,7 @@ import agentex.lib.adk as adk from agentex.lib.adk import create_langgraph_tracing_handler, stream_langgraph_events from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config from agentex.lib.sdk.fastacp.fastacp import FastACP -from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams +from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams from agentex.lib.types.fastacp import AsyncACPConfig from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger 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 b63683da1..5692396b2 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 @@ -28,7 +28,7 @@ from agentex.lib.adk import ( stream_pydantic_ai_events, create_pydantic_ai_tracing_handler, ) -from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams +from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams from agentex.lib.types.fastacp import AsyncACPConfig from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger diff --git a/src/agentex/lib/cli/templates/default/project/acp.py.j2 b/src/agentex/lib/cli/templates/default/project/acp.py.j2 index 5478b51b5..b0da14a5c 100644 --- a/src/agentex/lib/cli/templates/default/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/default/project/acp.py.j2 @@ -1,6 +1,6 @@ from agentex.lib.sdk.fastacp.fastacp import FastACP from agentex.lib.types.fastacp import AsyncACPConfig -from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams +from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent from agentex.lib import adk 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 2edd20fcb..54538d0c9 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 @@ -11,7 +11,7 @@ import agentex.lib.adk as adk from agentex.lib.adk import create_langgraph_tracing_handler, convert_langgraph_to_agentex_events from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config from agentex.lib.sdk.fastacp.fastacp import FastACP -from agentex.lib.types.acp import SendMessageParams +from agentex.protocol.acp import SendMessageParams from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.task_message_content import TaskMessageContent 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 0b3b482fe..4e2517838 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 @@ -4,7 +4,7 @@ from typing import AsyncGenerator, List from agentex.lib import adk from agentex.lib.adk.providers._modules.sync_provider import SyncStreamingProvider, convert_openai_to_agentex_events from agentex.lib.sdk.fastacp.fastacp import FastACP -from agentex.lib.types.acp import SendMessageParams +from agentex.protocol.acp import SendMessageParams from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.model_utils import BaseModel 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 e07f57a1a..4925e847f 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,7 +22,7 @@ from agentex.lib.adk import ( create_pydantic_ai_tracing_handler, convert_pydantic_ai_to_agentex_events, ) -from agentex.lib.types.acp import SendMessageParams +from agentex.protocol.acp import SendMessageParams from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.lib.sdk.fastacp.fastacp import FastACP 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 7184d26aa..ce5069a4c 100644 --- a/src/agentex/lib/cli/templates/sync/project/acp.py.j2 +++ b/src/agentex/lib/cli/templates/sync/project/acp.py.j2 @@ -1,6 +1,6 @@ from typing import AsyncGenerator, Union from agentex.lib.sdk.fastacp.fastacp import FastACP -from agentex.lib.types.acp import SendMessageParams +from agentex.protocol.acp import SendMessageParams from agentex.types.task_message_update import TaskMessageUpdate from agentex.types.task_message_content import TaskMessageContent 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 5b95d4479..2b81bb335 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 @@ -4,7 +4,7 @@ import os from temporalio import workflow from agentex.lib import adk -from agentex.lib.types.acp import CreateTaskParams, SendEventParams +from agentex.protocol.acp import CreateTaskParams, SendEventParams from agentex.lib.core.temporal.workflows.workflow import BaseWorkflow from agentex.lib.core.temporal.types.workflow import SignalName from agentex.lib.utils.logging import make_logger 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 23e5156f1..66a91d7a8 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 @@ -23,7 +23,7 @@ from temporalio import workflow from project.agent import TaskDeps, temporal_agent from agentex.lib import adk -from agentex.lib.types.acp import SendEventParams, CreateTaskParams +from agentex.protocol.acp import SendEventParams, CreateTaskParams from agentex.lib.types.tracing import SGPTracingProcessorConfig from agentex.lib.utils.logging import make_logger from agentex.types.text_content import TextContent diff --git a/src/agentex/lib/cli/templates/temporal/project/workflow.py.j2 b/src/agentex/lib/cli/templates/temporal/project/workflow.py.j2 index ad756eb15..56db5abf3 100644 --- a/src/agentex/lib/cli/templates/temporal/project/workflow.py.j2 +++ b/src/agentex/lib/cli/templates/temporal/project/workflow.py.j2 @@ -3,7 +3,7 @@ import json from temporalio import workflow from agentex.lib import adk -from agentex.lib.types.acp import CreateTaskParams, SendEventParams +from agentex.protocol.acp import CreateTaskParams, SendEventParams from agentex.lib.core.temporal.workflows.workflow import BaseWorkflow from agentex.lib.core.temporal.types.workflow import SignalName from agentex.lib.utils.logging import make_logger diff --git a/src/agentex/lib/types/acp.py b/src/agentex/lib/types/acp.py index 272521ad2..74295ef88 100644 --- a/src/agentex/lib/types/acp.py +++ b/src/agentex/lib/types/acp.py @@ -5,6 +5,8 @@ """ from agentex.protocol.acp import ( # noqa: F401 + RPC_SYNC_METHODS, + PARAMS_MODEL_BY_METHOD, RPCMethod, SendEventParams, CancelTaskParams, diff --git a/src/agentex/protocol/json_rpc.py b/src/agentex/protocol/json_rpc.py index 658c937f2..be03a4936 100644 --- a/src/agentex/protocol/json_rpc.py +++ b/src/agentex/protocol/json_rpc.py @@ -2,7 +2,13 @@ from typing import Any, Literal -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict + +# Preserve the config the previous `agentex.lib.utils.model_utils.BaseModel` +# applied — `from_attributes=True` lets callers `model_validate` from +# attribute-bearing objects (not just dicts); `populate_by_name=True` is a +# harmless default future-proofing for any field aliases. +_PROTOCOL_MODEL_CONFIG = ConfigDict(from_attributes=True, populate_by_name=True) class JSONRPCError(BaseModel): @@ -14,6 +20,8 @@ class JSONRPCError(BaseModel): data: The error data """ + model_config = _PROTOCOL_MODEL_CONFIG + code: int message: str data: Any | None = None @@ -29,6 +37,8 @@ class JSONRPCRequest(BaseModel): id: The ID of the request """ + model_config = _PROTOCOL_MODEL_CONFIG + jsonrpc: Literal["2.0"] = "2.0" method: str params: dict[str, Any] @@ -45,6 +55,8 @@ class JSONRPCResponse(BaseModel): id: The ID of the request """ + model_config = _PROTOCOL_MODEL_CONFIG + jsonrpc: Literal["2.0"] = "2.0" result: dict[str, Any] | None = None error: JSONRPCError | None = None From d56d08826cea515f6a84d15509bfa89db77cebfb Mon Sep 17 00:00:00 2001 From: Max Parke Date: Wed, 27 May 2026 10:42:02 -0400 Subject: [PATCH 3/5] test(types): pin back-compat shim contract for agentex.lib.types.{acp,json_rpc} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codifies the invariants Greptile flagged in PR review (and that the previous commit fixed): 1. Every symbol the original modules exported must be importable from the shim path — including the two module-level constants (RPC_SYNC_METHODS, PARAMS_MODEL_BY_METHOD) that an earlier shim iteration dropped. 2. The shim re-exports must be the *same* class objects as the canonical path (identity check, not just type equality). Different objects would silently break isinstance/match-case for any consumer that mixes import styles. 3. The pydantic ConfigDict (from_attributes=True, populate_by_name=True) that JSONRPCRequest/Response/Error inherited from model_utils.BaseModel before the refactor stays preserved on the canonical agentex.protocol.json_rpc classes. Verified locally: - All 5 tests pass against the current shim. - Simulated regression (removing RPC_SYNC_METHODS from the shim re-exports): 2 tests fail with the exact ImportError + AttributeError a downstream consumer would hit. Catches the contract violation. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_protocol_shims.py | 98 ++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/test_protocol_shims.py diff --git a/tests/test_protocol_shims.py b/tests/test_protocol_shims.py new file mode 100644 index 000000000..18f3953ce --- /dev/null +++ b/tests/test_protocol_shims.py @@ -0,0 +1,98 @@ +"""Tests that pin the back-compat contract for protocol-type shims. + +The canonical location for wire-protocol shapes is :mod:`agentex.protocol` +(see PR scaleapi/scale-agentex-python#371). The historical locations +:mod:`agentex.lib.types.acp` and :mod:`agentex.lib.types.json_rpc` are +preserved as re-export shims so external consumers' existing imports +continue to work. + +These tests enforce two invariants: + +1. **Symbol parity** — every public name the original modules exported + is still importable from the old path. Greptile flagged + ``RPC_SYNC_METHODS`` and ``PARAMS_MODEL_BY_METHOD`` as missing in an + earlier pass; this test prevents that regression. +2. **Identity** — the class objects at the shim path are the *same* + objects as the canonical path. Without this, type-narrowing via + ``isinstance`` or pattern matching would silently misbehave for code + that mixes import styles. + +Also asserts the :class:`pydantic.ConfigDict` settings on the JSON-RPC +classes survived the move from :mod:`agentex.lib.utils.model_utils` to +plain :mod:`pydantic` — Greptile flagged the silent loss of +``from_attributes=True`` / ``populate_by_name=True``. +""" + +from __future__ import annotations + + +def test_acp_shim_re_exports_all_original_symbols() -> None: + """Every name historically exported from agentex.lib.types.acp must + still be importable from that path via the back-compat shim.""" + # Importing each symbol; ImportError here means the shim regressed. + from agentex.lib.types.acp import ( # noqa: F401 + PARAMS_MODEL_BY_METHOD, + RPC_SYNC_METHODS, + CancelTaskParams, + CreateTaskParams, + RPCMethod, + SendEventParams, + SendMessageParams, + ) + + +def test_json_rpc_shim_re_exports_all_original_symbols() -> None: + """Every name historically exported from agentex.lib.types.json_rpc + must still be importable from that path via the back-compat shim.""" + from agentex.lib.types.json_rpc import ( # noqa: F401 + JSONRPCError, + JSONRPCRequest, + JSONRPCResponse, + ) + + +def test_acp_shim_classes_are_identical_to_canonical() -> None: + """Shim re-exports must be the *same* class objects as the canonical + path. Different objects would break ``isinstance`` for code that + mixes import styles.""" + from agentex.lib.types import acp as shim + from agentex.protocol import acp as canon + + assert shim.RPCMethod is canon.RPCMethod + assert shim.CreateTaskParams is canon.CreateTaskParams + assert shim.SendMessageParams is canon.SendMessageParams + assert shim.SendEventParams is canon.SendEventParams + assert shim.CancelTaskParams is canon.CancelTaskParams + assert shim.RPC_SYNC_METHODS is canon.RPC_SYNC_METHODS + assert shim.PARAMS_MODEL_BY_METHOD is canon.PARAMS_MODEL_BY_METHOD + + +def test_json_rpc_shim_classes_are_identical_to_canonical() -> None: + """Same identity check for the JSON-RPC envelope types.""" + from agentex.lib.types import json_rpc as shim + from agentex.protocol import json_rpc as canon + + assert shim.JSONRPCError is canon.JSONRPCError + assert shim.JSONRPCRequest is canon.JSONRPCRequest + assert shim.JSONRPCResponse is canon.JSONRPCResponse + + +def test_json_rpc_classes_preserve_legacy_model_config() -> None: + """Pre-refactor, JSON-RPC classes inherited + ``from_attributes=True`` / ``populate_by_name=True`` from + ``agentex.lib.utils.model_utils.BaseModel``. The refactor swapped + to plain ``pydantic.BaseModel`` and set ``model_config`` explicitly + to preserve both flags. Catch any future drop.""" + from agentex.protocol.json_rpc import ( + JSONRPCError, + JSONRPCRequest, + JSONRPCResponse, + ) + + for cls in (JSONRPCError, JSONRPCRequest, JSONRPCResponse): + assert cls.model_config.get("from_attributes") is True, ( + f"{cls.__name__}.model_config dropped from_attributes=True" + ) + assert cls.model_config.get("populate_by_name") is True, ( + f"{cls.__name__}.model_config dropped populate_by_name=True" + ) From d1ad15f630ec97e3016997406eccc34495b9ec79 Mon Sep 17 00:00:00 2001 From: Max Parke Date: Wed, 27 May 2026 10:45:28 -0400 Subject: [PATCH 4/5] fix(lint): sort imports in test_protocol_shims.py per ruff length-sort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three I001 errors from `ruff check` on the new test file — length-sort ordering inside each `from X import (a, b, c)` block. Auto-fixed via `ruff check --fix`; test still passes after sort. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_protocol_shims.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_protocol_shims.py b/tests/test_protocol_shims.py index 18f3953ce..e5e651b68 100644 --- a/tests/test_protocol_shims.py +++ b/tests/test_protocol_shims.py @@ -31,12 +31,12 @@ def test_acp_shim_re_exports_all_original_symbols() -> None: still be importable from that path via the back-compat shim.""" # Importing each symbol; ImportError here means the shim regressed. from agentex.lib.types.acp import ( # noqa: F401 - PARAMS_MODEL_BY_METHOD, RPC_SYNC_METHODS, - CancelTaskParams, - CreateTaskParams, + PARAMS_MODEL_BY_METHOD, RPCMethod, SendEventParams, + CancelTaskParams, + CreateTaskParams, SendMessageParams, ) @@ -55,8 +55,8 @@ def test_acp_shim_classes_are_identical_to_canonical() -> None: """Shim re-exports must be the *same* class objects as the canonical path. Different objects would break ``isinstance`` for code that mixes import styles.""" - from agentex.lib.types import acp as shim from agentex.protocol import acp as canon + from agentex.lib.types import acp as shim assert shim.RPCMethod is canon.RPCMethod assert shim.CreateTaskParams is canon.CreateTaskParams @@ -69,8 +69,8 @@ def test_acp_shim_classes_are_identical_to_canonical() -> None: def test_json_rpc_shim_classes_are_identical_to_canonical() -> None: """Same identity check for the JSON-RPC envelope types.""" - from agentex.lib.types import json_rpc as shim from agentex.protocol import json_rpc as canon + from agentex.lib.types import json_rpc as shim assert shim.JSONRPCError is canon.JSONRPCError assert shim.JSONRPCRequest is canon.JSONRPCRequest From 9f237a0db9e423fbb8ebf435edda5274daf06a6c Mon Sep 17 00:00:00 2001 From: Max Parke Date: Wed, 27 May 2026 10:58:11 -0400 Subject: [PATCH 5/5] docs(claude): document agentex.protocol/* canonical location + shim policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Declan's review on this PR — document the protocol-types migration in CLAUDE.md so future contributors know: - `src/agentex/protocol/` is the canonical home for slim-safe wire types (acp.py + json_rpc.py); imports allowed from a future REST-only install. - `src/agentex/lib/types/{acp,json_rpc}.py` are back-compat shims — re-exporting from the canonical path. Existing user imports unaffected; new code should target agentex.protocol.*. - Other `lib/types/*` modules (tracing, agent_card, credentials, fastacp, llm_messages, converters) stay in place because they have heavier transitive deps that aren't slim-safe. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 7e0df1779..7f62a42fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,11 +55,27 @@ The package provides the `agentex` CLI with these main commands: ### Code Structure - `/src/agentex/` - Core SDK and generated API client code +- `/src/agentex/protocol/` - **Canonical** location for wire-protocol shapes + (JSON-RPC envelopes, ACP method-param types). Depends only on `pydantic` + and the Stainless-generated `agentex.types.*` surface, so it is safe to + import from a future slim REST-only install. + - `acp.py` - `RPCMethod`, `CreateTaskParams`, `SendMessageParams`, + `SendEventParams`, `CancelTaskParams`, `RPC_SYNC_METHODS`, + `PARAMS_MODEL_BY_METHOD` + - `json_rpc.py` - `JSONRPCRequest`, `JSONRPCResponse`, `JSONRPCError` - `/src/agentex/lib/` - Custom library code (not modified by code generator) - `/cli/` - Command-line interface implementation - `/core/` - Core services, adapters, and temporal workflows - `/sdk/` - SDK utilities and FastACP implementation - `/types/` - Custom type definitions + - `acp.py`, `json_rpc.py` - **back-compat shims** re-exporting from + `agentex.protocol.*`. Existing `from agentex.lib.types.{acp,json_rpc} + import ...` keeps working; new code should import from the canonical + `agentex.protocol.*` paths. + - Other modules (`tracing`, `agent_card`, `credentials`, `fastacp`, + `llm_messages`, `converters`, etc.) stay here — they have heavier + transitive deps (temporal, openai-agents, model_utils/yaml) and + aren't slim-safe. - `/utils/` - Utility functions - `/examples/` - Example implementations and tutorials - `/tests/` - Test suites