feat(dev): run agentex locally without Docker #353
Conversation
| if raw is None and agent.production_deployment_id: | ||
| deployment = await self.deployment_repo.get( | ||
| id=agent.production_deployment_id | ||
| ) | ||
| if deployment.acp_url: | ||
| return deployment.acp_url | ||
| raw = deployment.acp_url | ||
|
|
||
| # Legacy fallback to the agent's own URL. | ||
| if raw is None: | ||
| raw = agent.acp_url | ||
|
|
||
| # Legacy fallback | ||
| if agent.acp_url: | ||
| return agent.acp_url | ||
| if raw is None: | ||
| raise ClientError(f"Agent {agent.id} does not have an ACP URL configured") | ||
|
|
||
| raise ClientError(f"Agent {agent.id} does not have an ACP URL configured") | ||
| return resolve_acp_url(raw) |
There was a problem hiding this comment.
Empty-string
deployment.acp_url bypasses the agent.acp_url fallback
The old code guarded with if deployment.acp_url: (truthy), so a deployment whose acp_url column was persisted as "" would be skipped and the code would fall through to the agent's own URL. The new code only checks is None, so an empty string from the deployment is assigned to raw, and resolve_acp_url("") returns "" (the function short-circuits on not url). The result is the caller receives "" and the subsequent HTTP dial fails instead of gracefully falling back to agent.acp_url. If any existing deployment rows have an empty-string rather than NULL acp_url, this is a live regression.
Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/domain/use_cases/agents_acp_use_case.py
Line: 336-349
Comment:
**Empty-string `deployment.acp_url` bypasses the `agent.acp_url` fallback**
The old code guarded with `if deployment.acp_url:` (truthy), so a deployment whose `acp_url` column was persisted as `""` would be skipped and the code would fall through to the agent's own URL. The new code only checks `is None`, so an empty string from the deployment is assigned to `raw`, and `resolve_acp_url("")` returns `""` (the function short-circuits on `not url`). The result is the caller receives `""` and the subsequent HTTP dial fails instead of gracefully falling back to `agent.acp_url`. If any existing deployment rows have an empty-string rather than NULL `acp_url`, this is a live regression.
How can I resolve this? If you propose a fix, please make it concise.Stand up the full backend as host processes with embedded datastores — no Docker daemon required — as a lighter alternative to the container stack. `./dev.sh local` (also `make dev-local` / `python -m scripts.dev_local`) provisions: - Postgres via bundled pgserver (unix socket) and Redis via bundled redislite - a Temporal dev server + UI and the agentex worker (--no-temporal to skip) - a local mongod, required for the full stack (--no-mongo / --lean to skip) - an optional OpenTelemetry collector (--no-otel to skip) then runs migrations, supervises uvicorn + the worker, and tears everything down cleanly on SIGINT/SIGTERM. --lean is a minimal Postgres+Redis+API stack; --ephemeral uses a throwaway data dir. The runner is a small scripts/dev_local package (config / services / supervise / runner) so the pure config/env layer stays testable. App-side changes to make the no-Docker path robust (all no-ops when Mongo is configured, as it always is in Docker/prod): - The Temporal worker no longer crashes when MongoDB is unavailable — the Mongo CRUD adapter tolerates an unset database and errors only on real use, so the worker degrades like the API instead of taking down the stack. - Skip the Mongo connection entirely when MONGODB_URI is unset, removing a ~20s startup hang against the implicit localhost:27017 default. - In local mode the backend rewrites agents' host.docker.internal ACP host to loopback (AGENTEX_ACP_HOST_OVERRIDE), so default-scaffolded agents work without manifest edits. Also fix a frontend dev-server process leak in dev.sh (kill the whole make→npm→next tree and sweep orphans; report status by listening port), correct the MongoDB and OpenTelemetry install commands, and document local mode in README and CLAUDE.md.
356b3eb to
aa8bb61
Compare
| raw = acp_url_override | ||
|
|
||
| # Prefer the production deployment's URL when there's no explicit override. | ||
| if raw is None and agent.production_deployment_id: |
There was a problem hiding this comment.
@smoreinis can you take a look here just make sure it doesn't conflict with the preview workflow?
| """ | ||
| # In docker-free local mode, rewrite host.docker.internal -> the host-reachable | ||
| # override so the healthcheck matches how the request path dials the agent. | ||
| acp_url = resolve_acp_url(acp_url) |
There was a problem hiding this comment.
just making sure, no other places we need to do conversion right?
| if acp_url_override: | ||
| return acp_url_override | ||
| """Resolve the ACP URL for an agent, optionally overriding with a specific URL. | ||
|
|
There was a problem hiding this comment.
did we lose the override?
| Returns the URL unchanged when the override env var is unset or the URL does | ||
| not use the Docker sentinel host, so it is safe to call on every ACP dial. | ||
| """ | ||
| override = os.environ.get(_ACP_HOST_OVERRIDE_ENV) |
There was a problem hiding this comment.
is this where the override moved?
| # Development Server | ||
| # | ||
|
|
||
| dev: install-dev ## Start development server with Docker Compose |
There was a problem hiding this comment.
these are both technically local maybe rename to something more meaningful?
|
|
||
| > **MongoDB is required for the full local stack** and is always started — the Temporal | ||
| > worker builds Mongo-backed repositories at startup, so a missing/unreachable Mongo | ||
| > makes the runner fail fast (with an install message) rather than crash the worker. |
danielmillerp
left a comment
There was a problem hiding this comment.
lots of small questions! also as far as testing on PC, not super sure. A lot of our clients do use PCs and I know there are Scaliens who have PCs. What problems do you anticipate?
There was a problem hiding this comment.
Overall this is rad.
One UX suggestion: in addition dev.sh, I think this would be really nice to have in the agentex CLI. I.E. you can run a single agent inline. therefore to start running a single agent you can start quick rather than having both the agentex backend and cli running. then this setup would be suitable for someone testing a multiagent flow.
Not sure if that is possible given the abstraction and integration, but wanted to throw it out there as a north star.
+1 to that! |
What
Adds a docker-free local mode that runs the full agentex backend as host processes
with embedded datastores — a lighter alternative to the Docker Compose stack, closer
to a one-command
langgraph dev-style workflow.Also available as
make dev-localandpython -m scripts.dev_local.Why
Standing up a local environment previously required the full Docker stack. This lets a
developer run the backend with a single command and no Docker daemon:
pgserver(unix socket) and Redis via bundledredislitemongod— always started; the stack requires it (the Temporal worker buildsMongo-backed repositories at boot)
It runs migrations, supervises uvicorn + the worker, and tears everything down cleanly
on Ctrl-C / SIGTERM.
--ephemeraluses a throwaway data dir;--mongo-uripoints at anexisting MongoDB instead of launching a local
mongod.App-side change
Safe no-op wherever the backend runs in Docker / staging / prod (the env var it keys on
is unset there):
host.docker.internal(the SDK default, so a Docker backend can reach an agent onthe host), which a host-process backend can't resolve. The runner sets
AGENTEX_ACP_HOST_OVERRIDE=127.0.0.1and the backend rewrites only that sentinel hostto the override when dialing agents — in the ACP request path, the agent-API-key proxy
path, and the Temporal healthcheck. When the env var is unset, the stored URL is used
verbatim. Default-scaffolded agents work without manifest edits.
Also
dev.sh(kill the whole make → npm → nexttree and sweep orphans; report status by listening port).
brew trustfirst) and OpenTelemetry (release binary; not inHomebrew) install commands.
dev-localuv dependency group (pgserver,redislite,greenlet) pulled onlyfor local mode.
README.mdandCLAUDE.md.Contract note
The runner is a package (
scripts/dev_local/), so the direct invocation ispython -m scripts.dev_local(notpython scripts/dev_local.py)../dev.sh localandmake dev-localare unchanged for callers.Testing
Manually exercised on macOS (Docker stopped):
--lean— the API passes/healthzwithin ~1s and/readyzreportsPostgres, Redis, and MongoDB all healthy; the Temporal worker stays up; teardown frees
all ports with no orphaned processes.
mongodabsent aborts with an actionable install message(or point at an external instance with
--mongo-uri).python -m scripts.dev_local; agents scaffolded byagentex initconnect without manifest edits (ACP host rewrite).