SmolPaws · architecture · migration plan

Swapping SmolPaws onto the new SDK

SmolPaws runs on an old TypeScript agent SDK. A fresh, cleaner transpile now exists (@smolpaws/openhands-agent). This page is the map for moving onto it — what has drifted apart, where the pain is, and the order we do it in so we can keep talking and working the whole way through.

★ The one thing to know

The new SDK already knows how to talk to an agent-server — but it speaks a different dialect than the one SmolPaws actually runs. The old world sends “turns”; the new world sends “events + run”. Sealing that one seam is the hinge the whole migration turns on.

1 · The big picture: three repositories

There are three moving parts. Only one of them is really “ours to keep and grow”, one is the destination, and one is just a reference we no longer build on.

@smolpaws/agent-sdk

The old TypeScript SDK, living inside the VS Code extension repo. What SmolPaws imports today.

reference / leaving

@smolpaws/openhands-agent

The new fresh transpile of the Python SDK. Cleaner, tested, zod-backed. Where we’re going.

destination

smolpaws / apps/agent-server

The server + bridges that are SmolPaws. This is ours alone — the new SDK doesn’t include a server.

ours to keep

The new SDK is a library, not a server. It ships the client side of the server boundary (it can talk to an agent-server), but the agent-server itself — process execution, files, the bridges, the whole SmolPaws body — stays in our repo. So this isn’t “replace SmolPaws with the new thing”. It’s “re-floor SmolPaws’ engine room while the house stays standing.”

openhands-agent · docs/ARCHITECTURE.md → “does not contain the Python openhands-agent-server”

2 · The drift to seal: “turns” vs “events + run”

Upstream (the Python SDK) never had a notion of a turn. You send a message as an event, then tell the conversation to run. SmolPaws grew its own richer idea: a turn — a first-class, numbered, persisted unit with a status (running, completed, paused, stuck…), an idempotency key, and a “delivery owner” so exactly one bridge delivers the reply.

That was a deliberate, useful drift — it’s how a WhatsApp message becomes exactly one reply even across restarts. But it means our server and the new SDK’s client no longer shake hands. Here is the exact mismatch, from the real code on both sides:

The seam: SmolPaws speaks /turns, the new SDK speaks /events + /run A bridge posts a turn to SmolPaws today; the new SDK RemoteConversation posts events then run. The two contracts do not match. TODAY — WHAT SMOLPAWS RUNS Bridge (WhatsApp…) POST /turns SmolPaws agent-server /turns /turns/:id/result …/outbound_messages/claim …/task_commands/claim turn = first-class, polled poll + claim reply NEW SDK — WHAT IT EXPECTS RemoteConversation (new SDK client) POST /events POST /run agent-server /events · /run /pause · /interrupt no “turn” — send & run These two don’t match. Sealing the seam = making them agree.
The core drift. Left in, grounded to code below — the paths on the top row are real routes SmolPaws serves; the bottom row is exactly what the new SDK’s client calls.

SmolPaws today

A turn is its own object with status, sequence, idempotency key and a delivery owner.

turnState.ts · conversationRouter.ts:237 — POST /turns · turnClient.ts (submit → poll → claim)

New SDK client

Sends an event, then a run; pause/interrupt to control. No turn object at all.

remote-conversation.ts → /events, /run, /pause, /interrupt

3 · The other gap: we lost the OpenAPI contract

Upstream the server’s API is described by an OpenAPI schema, generated automatically — a machine-readable contract clients can trust. SmolPaws’ server has no OpenAPI today. That’s the second thing to restore (or better: define), because it’s what lets the new SDK’s client and our server agree on shapes without guessing.

Python agent-server generates OpenAPI via openapi.py — SmolPaws’ agent-server has no equivalent spec.

4 · The plan, in order

Seven steps, kept exactly as scoped — not split finer. The ordering is chosen so the lifeline (being able to message SmolPaws and get work done) is restored as early as possible, and the genuinely hard part is faced with eyes open rather than stumbled into. No time estimates — this is about sequence and dependency, not calendar.

  1. Swap openhands-agent in for the old agent-sdk first

    Change what the server imports: the new SDK becomes the engine. Before touching any bridge behaviour, get the core running on the new library. Doing this first surfaces exactly what the server actually needs from the SDK, which tells us how far the next step has to go.

  2. Agent-server parity (ish)

    Bring the new SDK’s world close enough to how our agent-server behaves that the two can meet. “Ish” is honest: not every upstream surface, just enough to build on — and this is where the sibling takes on the agent-server layer the SDK transpile deliberately skipped. Scope is driven by what step 1 revealed the server truly relies on.

  3. Build durable message work around agent-server ADR proposed

    The design is now explicit: one shared SQLite-backed Message Work Coordinator lives in the SmolPaws bridge runtime, outside the upstream-shaped agent-server. Platform adapters compute canonical lanes and accept/deliver through it; they do not own retry loops. For webhook ingress, a Cloudflare Queue is an optional edge spool — the local pull adapter acknowledges it only after the coordinator commits intake. It replaces the fragile Quick Tunnel, but does not replace the coordinator. Read the message-work ADR →

  4. Swap agent-server + WhatsApp lifeline restored

    Do the smallest end-to-end slice that lets us keep talking: the new server plus the WhatsApp adapter through the coordinator. Once this works, SmolPaws and Engel can continue working together on the new stack — everything after this happens with the lifeline intact.

  5. Then Discord, GitHub, Email

    Migrate the remaining platform adapters onto the same coordinator interface, one at a time. GitHub and email can use Worker → Cloudflare Queue → local HTTP pull; Discord remains a connected adapter. The transport differs, but durable acceptance begins at the same coordinator seam.

  6. Slack-via-Chrome should keep working untouched verify, don’t assume

    The Slack SmolPaws reads today (the OpenHands workspace, during heartbeats) goes through a browser — the Chrome tab and Slack’s web API — not through the agent-server at all. So the SDK swap shouldn’t touch it: it likely just keeps working. “Probably” is still a hypothesis to confirm once the new stack is live, not a step to skip.

  7. Retest, adapt, redeploy paws — the standalone Slack app

    paws is the separate Slack bot app (apps/slack) — SmolPaws’ own thinner Slack surface. It isn’t in active use yet, but it will be, and it does ride the agent-server through the bridge adapter. So once the new stack is proven, paws gets re-tested against it, adapted at its seams, and redeployed. Outward-facing cat, so it goes last and carefully.

5 · Why step 3 is the one to respect

Everything hinges on replacing what “turns” quietly did for us. It wasn’t just bookkeeping — it bundled source deduplication, execution ownership and reply delivery into one object. The replacement must separate those facts instead of recreating a shadow turn:

The guarantee is deliberately honest: intake becomes effectively-once once deterministic append exists. External delivery is effectively-once only when the platform supports idempotency or reconciliation; an ambiguous send becomes delivery_unknown, never a blind retry. WhatsApp is still the right first proof because a dropped, doubled or reordered message is immediately visible.

⚠ The trap to avoid

Don’t make the new SDK speak /turns, put platform queue state inside agent-server, or mistake Cloudflare Queue for the coordinator. The point is to retire the turn abstraction while preserving each fact at its natural seam. The full contract and crash matrix live in the durable message-work ADR.

6 · What’s already true today

Not starting from zero. The new SDK’s provider routing is done and verified live (OpenAI, Anthropic, Gemini all dispatch to the right client). Its conversation loop, tools, workspace and skills are ported and tested. The shared bridge loader, registry, base adapter and turn client exist; Discord uses the shared dispatch path, while Slack only shares lifecycle/output and still has its own client. WhatsApp remains the legacy SQLite/poll monolith; GitHub already has a Cloudflare Queue but its Worker consumer still calls the runner directly; email also dispatches directly. The coordinator interface and SQLite work store are designed, not implemented. The old unmerged burst-coalescing branch must be reconsidered: the ADR preserves each asynchronous message boundary instead of merging messages in adapter memory.