--- name: agentex description: Use when building, wiring, or debugging an Agentex agent — choosing agent type, configuring acp.py and manifest.yaml, using adk.messages or adk.state, or resolving Windows-specific setup issues. --- # Agentex Platform Agentex is a platform for building and deploying intelligent agents. The repo has two main parts: - `agentex/` — FastAPI backend + Temporal workflows (runs in Docker) - `agentex-ui/` — Next.js frontend (runs locally) Agents are built with the `agentex-sdk` CLI and run as separate processes that register with the backend. ## When to Activate - Choosing between sync, async, or Temporal agent type for a new agent - Wiring `acp.py`, `manifest.yaml`, or `run_worker.py` for a new agent - Using `adk.messages`, `adk.state`, or `adk.providers` in an activity or workflow - Debugging ACP protocol issues or agent registration failures - Understanding the backend DDD layer boundaries or exception mapping - Windows-specific setup issues (`uv sync`, port conflicts, `.env` loading) --- ## Agent Types ### Sync ACP One message in, one response out. Stateless. ```python acp = FastACP.create(acp_type="sync") @acp.on_message_send async def handle(params: SendMessageParams) -> TaskMessageContent: return TextContent(author="agent", content="reply") ``` **Use when:** FAQ bots, translation, data lookups, single-turn interactions. ### Async ACP (base) Task lifecycle with persistent state across multiple turns. ```python acp = FastACP.create(acp_type="async", config=AsyncACPConfig(type="base")) @acp.on_task_create # called once — initialize state @acp.on_task_event_send # called per message — respond via adk.messages.create @acp.on_task_cancel # called on cancel — cleanup ``` Key difference from sync: responses are **pushed** via `adk.messages.create`, not returned. State is persisted via `adk.state.create / get_by_task_and_agent / update`. **Use when:** multi-turn conversations, stateful workflows, streaming LLM responses. **Warning:** race conditions if parallel events arrive — use Temporal for production. ### Async ACP + Temporal Same as Async but every step is a durable Temporal workflow. Survives crashes and restarts. ```yaml # manifest.yaml agent: acp_type: async temporal: enabled: true ``` **Use when:** production agents, long-running tasks, human-in-the-loop, complex multi-step tool chains. --- ## ACP State Pattern (Async) ```python class MyState(BaseModel): turn: int messages: List[Message] # Create on task init await adk.state.create(task_id=..., agent_id=..., state=MyState(...)) # Read on each event task_state = await adk.state.get_by_task_and_agent(task_id=..., agent_id=...) state = MyState.model_validate(task_state.state) # Write back after mutating await adk.state.update(state_id=task_state.id, task_id=..., agent_id=..., state=state) ``` --- ## Sending Messages (Async) ```python # Echo user message back (so it shows in UI) await adk.messages.create(task_id=params.task.id, content=params.event.content) # Send agent reply await adk.messages.create( task_id=params.task.id, content=TextContent(author="agent", content="response text"), ) # Streaming LLM (auto-sends chunks to UI) await adk.providers.litellm.chat_completion_stream_auto_send( task_id=params.task.id, llm_config=LLMConfig(model="gpt-4o-mini", messages=state.messages, stream=True), ) ``` --- ## manifest.yaml Structure ```yaml local_development: agent: port: 8000 # must be unique per agent (8000, 8001, 8002...) host_address: host.docker.internal paths: acp: project/acp.py agent: name: my-agent # unique name, shown in UI acp_type: sync # or async temporal: enabled: false credentials: [] env: {} ``` --- ## Backend Architecture ``` src/ ├── api/routes/ # FastAPI endpoints ├── domain/entities/ # Pure Pydantic models ├── domain/use_cases/ # Business logic ├── adapters/crud_store/ # DB adapters (Postgres + MongoDB) ├── adapters/streams/ # Redis SSE streams └── config/dependencies.py # Singleton GlobalDependencies ``` **Layer rules:** - Domain layer has zero framework imports - API layer → use cases → domain ← adapters - ORM ↔ domain conversion via explicit converter functions — never skip layers **Exceptions:** - `ClientError` → 400, `ServiceError` → 500, `ItemDoesNotExist` → 404 --- ## Windows-Specific Gotchas | Problem | Fix | |---|---| | `uv sync` fails: platform not compatible | Add `"sys_platform == 'win32'"` to `environments` in root `pyproject.toml`, then `uv lock` | | `load_dotenv(override=True)` clobbers Docker env vars | Change to `override=False` in `environment_variables.py` | | Local PostgreSQL on port 5432 blocks Docker | Change Docker postgres port to `5434:5432` in `docker-compose.yml` | | `agentex init` Unicode error | Set `$env:PYTHONUTF8 = "1"` before running | | `agentex init` path has `\n` in it | Type short relative name (`my-agent`), not a full path | | `source .venv/bin/activate` fails | Use `.venv\Scripts\Activate.ps1` on Windows | | Temporal worker connects to `localhost` inside Docker | Caused by `.env` overriding Docker network hostnames — needs `override=False` | --- ## Ports | Port | Service | |---|---| | 3000 | Frontend UI | | 5003 | FastAPI backend (Swagger at /swagger) | | 5432 | Local PostgreSQL (if installed) | | 5434 | Docker agentex-postgres (remapped to avoid conflict) | | 5433 | Docker Temporal PostgreSQL | | 6379 | Redis | | 7233 | Temporal server | | 8080 | Temporal UI | | 8000+ | Agent ACP servers (one port per agent) | | 27017 | MongoDB | --- ## Key Environment Variables (agentex/.env) ```env ENVIRONMENT=development DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5434/agentex TEMPORAL_ADDRESS=localhost:7233 REDIS_URL=redis://localhost:6379 MONGODB_URI=mongodb://localhost:27017 MONGODB_DATABASE_NAME=agentex AGENTEX_SERVER_TASK_QUEUE=agentex-server ALLOWED_ORIGINS=http://localhost:3000 ENABLE_HEALTH_CHECK_WORKFLOW=true ``` --- ## Running Tests ```powershell cd agentex # Unit tests (no Docker needed) .\build.ps1 test-unit # Integration tests (needs Docker infra running) .\build.ps1 test-integration # Specific file .\build.ps1 test -File tests/unit/test_foo.py ``` --- ## Red Flags - **Sync ACP for multi-turn conversations** — sync agents receive one message and return one reply; they have no state, no turn history, and no mechanism to stream responses; use async ACP (or async + Temporal) for any stateful interaction - **Handler decorators in `acp.py` for a Temporal agent** — Temporal agents route all ACP events through the workflow engine; registering `@acp.on_task_create` decorators in `acp.py` bypasses Temporal and runs handlers outside the durable execution context - **Returning a response from an async handler instead of using `adk.messages.create`** — async agent handlers are not expected to return a value; the return value is silently discarded and the user sees no reply; push responses explicitly via `adk.messages.create` - **Not following load → mutate → save with `adk.state`** — reading state, mutating it in-memory, and then returning without saving means the next signal handler loads stale state; always call `adk.state.update` after every mutation before returning - **Omitting `get_all_activities()` in `run_worker.py`** — ADK built-in activities (messages, state persistence, tracing) are registered via `get_all_activities()`; omitting it means all `adk.messages.create` and `adk.state.*` calls fail at runtime with "activity not found" - **Two agents sharing the same port in `manifest.yaml`** — each ACP server process binds a port; running two agents with the same `local_development.agent.port` causes one to fail to start; increment the port for each agent (8000, 8001, 8002, …) - **`load_dotenv(override=True)` when running inside Docker** — overriding with the local `.env` file replaces Docker-injected environment variables such as `DATABASE_URL` and `TEMPORAL_ADDRESS` with localhost values, breaking service discovery inside the container network ## Checklist - [ ] `acp_type` chosen correctly in `manifest.yaml` (sync / async / async + temporal) - [ ] Temporal agent `acp.py` has only `FastACP.create(acp_type="async", config=TemporalACPConfig(...))` — no handler decorators - [ ] `adk.messages.create` used to send responses (not returned from handlers) - [ ] State follows load → mutate → save pattern via `adk.state` - [ ] `on_task_create` ends with `await workflow.wait_condition(lambda: self._done)` for Temporal agents - [ ] `get_all_activities()` included in worker alongside custom activities - [ ] Agent port in `manifest.yaml` is unique across all running agents (8000, 8001, …) - [ ] Windows: `load_dotenv(override=False)` to avoid clobbering Docker env vars - [ ] Domain exceptions (`ClientError`, `ServiceError`, `ItemDoesNotExist`) used — not `HTTPException` in use cases