feat(email): fast dev-iteration loop for the email agent SDK#2083
Conversation
… attach mode
Integrators who hit a bug in the frozen email sidecar had no way to fix it:
the binary is opaque, so the only recourse was to file an issue and wait for a
new binary + npm release — a multi-day loop. This adds a seconds-long loop that
runs the agent's Python source (which serves a contract identical to the frozen
binary) and drives it with the same shipped TS client. Dev and prod now differ
only in where the client points.
Python: move the full sidecar app wiring into an importable
`gaia_agent_email.server` (single source of truth) and reduce `packaging/server.py`
to a thin re-export so the freeze entry, `uvicorn server:app`, and the by-path
caller-auth test are unchanged. Add a `gaia-agent-email` console script with a
`serve` command supporting `--reload`/`--dev` for the hot-reload dev loop.
npm: add `connectSidecar({ baseUrl })` (attach mode — health + version check
against a server you already run, spawns nothing) and an `agent-email dev` CLI
that launches the source server. No wire change: SCHEMA_VERSION stays 2.3.
|
Verdict: Approve with suggestions. This PR adds a fast local dev loop for the email agent: a new importable The one thing worth fixing before merge: when 🔍 Technical details🟡 Important60s hang after an immediate launch failure ( const baseUrl = `http://${host}:${port}`;
const launchFailed = new Promise<never>((_, reject) => {
child.once("error", (e) => reject(e));
});
try {
const dev = await Promise.race([
connectSidecar({ baseUrl, healthTimeoutMs: 60_000 }),
launchFailed,
]);(then keep the existing 🟢 Minor
"caller-token off for local dev" comment slightly overstates Strengths
|
Review follow-ups on the fast-iteration loop: - `agent-email dev` now races readiness against an early child exit/spawn error, so a bad launcher (typo, missing package, taken port) fails immediately with an actionable ENOENT hint instead of waiting out the full health timeout. - Rewrote the "Fast local iteration" guide (email-integration.mdx + README) as a scannable numbered loop — the doc a developer iterating on the agent actually wants: one-line why, three copy-paste steps, one line back to production. - Doc accuracy: the sidecar app wiring + inline probes now live in `gaia_agent_email.server`, so the caller-auth docstrings and the capability matrix footnote point there (regenerated CAPABILITY_MATRIX.md) instead of `packaging/server.py`, which is now a thin re-export.
…ntract Second review pass, hardening edge cases: - `connectSidecar` now takes an `AbortSignal`, and `agent-email dev` uses it: when the launched server dies before it's ready, the health poll is cancelled instead of probing for the full 60s in the background (masked by process.exit in the CLI, but a real leak for programmatic callers). The losing race branch's rejection is pre-caught so it's never unhandled. - `serve --reload`/`--dev` now fails loud in a frozen binary (no source to watch, and uvicorn's reloader would re-exec the exe) instead of misbehaving. - Locked the frozen-binary invocation contract with a test: `main(["--host", H, "--port", P])` (no `serve` token, exactly how spawnSidecar and smoke_test.py launch it) normalizes to a serve run of the pre-built app. - Added a fast-fail test for `agent-email dev` (returns 1 well under the health timeout when the launcher can't be spawned).
A developer iterating on triage would otherwise be confused when the source server starts fine (health is liveness-only) but live triage returns 502 until a local Lemonade Server is up. One bullet, so they know to start Lemonade for LLM paths and can iterate on non-LLM code before then.
|
🟡 The Python package version wasn't bumped to match the npm package bump, which will break the release CI. The npm package went from Fix: bump 🔍 Technical details
The correct flow: update |
Closes a hidden CI gap. The sidecar-app contract tests (tests/unit/test_email_sidecar_devmode_app.py + _server_wiring.py) verify that `uvicorn server:app --app-dir <email>/packaging` resolves and that the live sidecar routes correctly — the exact contract the packaging/server.py shim + in-package build_app must uphold. But they live at tests/unit/ top level, which only test_unit.yml runs, and that workflow triggers on src/**/tests/** — NOT hub/agents/python/email/**. So a PR that refactors only the email package (like the dev-iteration server move) could break Agent UI dev mode with all its own checks green. Wire both guards into test_email_agent_unit.yml, which already triggers on the email package and installs core + the email package editable — so an email-package change now runs the dev-mode contract guard it can break.
Resolved README.md conflict by taking main's rewritten lean overview — main restructured the README to delegate detail to SPEC.md, and my "Fast local iteration" content already lives in SPEC.md + docs/guides/email-integration.mdx, matching that structure. Merge notes: - CHANGELOG: folded my dev-iteration entry into main's single 0.5.0 section (was duplicated), in main's plain-language voice; dropped the "SCHEMA_VERSION stays 2.3" claim since main's 0.5.0 also lands the 2.4 query endpoint. - Versions now align at 0.5.0 (main bumped py + npm; my [project.scripts] intact). - Verified the merged sidecar (my in-package build_app) still serves main's new /v1/email/query (2.4) — it's included into api_routes.router, which build_app mounts. Regenerated CAPABILITY_MATRIX.md. - npm build + 79 tests pass; hub email suite 577 pass (1 pre-existing env-only version-metadata failure, green in CI); Agent UI dev-mode guards pass.
kovtcharov
left a comment
There was a problem hiding this comment.
Concept and execution both look right: dev-source parity through the same shipped TS client is exactly the integrator story #1653/#1896 wanted, the fail-loud resolveBinaryPath guidance is good, and the npm suite passes on the branch (79 tests, verified locally). Doc surfaces updated together per the multi-surface rule.
One sequencing flag before merge: this branch predates both #2102 (the npm /query client — lifecycle.ts/index.ts/package-lock overlap) and #2060 (which moves the whole npm package to hub/agents/email/npm/). Merging order matters: rebase onto current main first (expect conflicts in lifecycle.ts/index.ts with #2102's additions — resolve by keeping both features), and coordinate with #2060 so whichever lands second re-runs its path/rename resolution. After the rebase the suite should be at 101+79-overlap tests, not 79 — worth confirming the /query tests still pass alongside the dev-loop ones.
|
Parking this until #2142 (V2-6: AgentSidecarManager + dev-mode registration) is picked up in the milestone-55 run — the |
test_shim_serves_identical_routes_to_in_package_builder failed on CI because its Starlette represents include_router-mounted routes as _IncludedRouter objects without a .path attribute — every /v1/email/* path read as None, so '/v1/email/triage' was never in the introspected set even though the route is mounted (the same run's --print-openapi test proves it serves). Locally a newer Starlette exposes .path, masking the failure. Assert parity the version-robust way the sibling dev-mode tests already do: builder identity (the shim re-exports build_app), OpenAPI-contract equality between both entry points, and TestClient reachability probes (!= 404) for the canonical surfaces.
Why this matters
Before: an app integrator who found a bug in the email agent was stuck — we ship the agent as a frozen, opaque binary, so their only recourse was to file an issue and wait days for a new binary + npm release. After: they run the agent's Python source (which serves a contract byte-for-byte identical to the frozen binary — the binary is that source frozen) and drive it with the same shipped TS client. Edit Python, it auto-reloads, re-test in seconds. Dev and prod differ only in where the client points.
No wire change:
SCHEMA_VERSIONstays 2.3,checkVersionstill guards drift, existing integrations are untouched, and no eval is required (transport/packaging only — no prompt/tool/model/error-classification change).What's in it
connectSidecar({ baseUrl, authToken? })(npm) — attach mode: health- and version-checks a server you already run and returns a bound client. Spawns nothing, nochild, nothing toshutdown(). The client half of the dev loop.gaia-agent-email serve --reload(Python) — a new console script that runs the source agent with hot reload (token-off for local dev). The full sidecar app wiring now lives in an importablegaia_agent_email.server;packaging/server.pyis a thin re-export, so the freeze entry,uvicorn server:app, and the by-path caller-auth test are unchanged.agent-email dev(npm CLI) — one command to launch the source server and print the base URL to attach.Test plan
cd hub/agents/npm/agent-email && npm run build && npm test— 78 pass (newconnect.test.ts+devlauncher tests)pytest hub/agents/python/email/tests/{test_server_cli,test_caller_auth,test_capability_matrix}.py— 35 passpython -m gaia_agent_email.export_openapi --check— no contract driftblack/isortclean on touched Python/health,/version(apiVersion 2.3),/openapi.jsonall serve the frozen-binary contractpip install -e hub/agents/python/email→gaia-agent-email serve --reload→connectSidecar({ baseUrl })→triage, then edit a triage heuristic and confirm reload serves the changeNotes for the reviewer
0.4.0(npm bumped to0.5.0); the new console script can fold into the next Python release bump. The npmdevcommand's default launcher (gaia-agent-email serve) needs the Python package built from this commit — a dev-only convenience;connectSidecaritself has no such coupling.