Counterpart page: OpenHands agent-server parity toward a replaceable SmolPaws server
Swapping the SDK: what smolpaws uses & where it lands
smolpaws imports 22 distinct symbols from @smolpaws/agent-sdk@0.10 across ~21 files. To hook it up to the fresh transpile @smolpaws/openhands-agent@0.1, every one needs a home. This is the wire-by-wire map: 13 map cleanly 4 renamed 3 to port 3 deviations to decide.
1The surface at a glance
Every symbol smolpaws pulls from the SDK, bucketed by how much work the swap needs. Green is a drop-in; the rest is the actual migration work.
2Clean map — drop-in 13
Same name, same shape, exported from the new package barrel. Change the import path, done.
| Symbol | New package location | smolpaws files |
|---|---|---|
AgentContext | context/agent-context.ts:22 | conversationRuntime |
BrowserTool | tools/index.ts:217 | conversationRuntime |
Event type | event/index.ts:248 | eventRouter, sockets, eventService |
FileStore | io/index.ts:8 | conversationService |
LocalConversation | conversation/local-conversation.ts:14 | conversationRuntime |
Skill | skills/index.ts:38 | projectSkills |
TextContent | llm/index.ts:146 | conversationRouter, eventRouter, sockets, turnState |
Message type | llm/index.ts:150 | (type re-export) |
ToolDefinition | tool/index.ts:58 | outboundMessaging, taskCommands |
MessageEvent type | event/index.ts:257 | (type re-export) |
loadSkillsFromDir | skills/index.ts:117 | projectSkills |
(13 counts the type/value pairs — Event, TextContent, MessageEvent, Message appear as both.)
3Renamed / reshaped 4
The concept exists but under a different name or shape. Each needs a small adapter or a rename at the call site.
| Old (0.10) | New (0.1) | Note | smolpaws files |
|---|---|---|---|
Workspace | BaseWorkspace / LocalWorkspace workspace:32,48 | base is now an interface; pick LocalWorkspace or RemoteWorkspace | conversationRuntime, shared-runner |
AgentServerWorkspace | RemoteWorkspace workspace:125 | the agent-server-backed workspace, renamed | (type) |
isAgentServerWorkspace | — (use instanceof RemoteWorkspace) | guard not exported; trivially replaced | shared-runner |
OpenHandsSettings | OpenHandsAgentSettings settings:64 | + openHandsAgentSettingsSchema (zod). Confirm field parity. | (type) |
4Must port 3
Genuinely absent from the new package. Small, pure helpers — port them, or add them upstream to the transpile.
| Symbol | What it does | smolpaws files |
|---|---|---|
isMessageEvent | event type guard — the new pkg has isAcpPatchEdit but not this family | activityRouter, conversationState |
isConversationStateUpdateEvent | event type guard | conversationState |
reduceTextContent | collapses TextContent[] → string; used all over the read path | activityRouter, conversationState, messageText |
event/ and llm/ modules and export them — they belong there, and it keeps smolpaws import-clean. (Good first upstream PRs to the transpile.)5Intentional deviations — decide 3
The transpile's README lists these as deliberately dropped: "no security analyzers, risk scoring, confirmation gates … Python secret-storage split." smolpaws imports all three. So this isn't "port it" — it's "do we still want it?"
| Symbol | Was | Decision | smolpaws files |
|---|---|---|---|
LLMSecurityAnalyzer | guardrail LLM / risk scoring | Dropped by design. Do we rely on it? If yes → keep as a smolpaws-side wrapper; if no → delete the wiring. | conversationRuntime |
createConfirmationPolicyFromSettings | confirmation gate before risky actions | Dropped by design. This is the "don't do irreversible things unsupervised" lever — decide consciously (see caution). | conversationRuntime |
SecretRegistry | Python secret-split registry | Replaced by the new SecretStore / keyring model (see the secrets page). Re-wire, don't port. | conversationRuntime |
clearRawLlmFieldsWhenProfileSelected | settings hygiene helper | Not in new pkg. Small — port it, or fold into the profile-selection path. | conversationRuntime |
6The hotspot: conversationRuntime.ts
Eight of the 22 symbols — and every one of the hard ones (security, confirmation, SecretRegistry, Workspace, clearRawLlm) — land in a single file. The other ~20 files each touch one or two easy symbols.
7Suggested migration order
- Port the 3 pure helpers (
isMessageEvent,isConversationStateUpdateEvent,reduceTextContent) into the new package & export them. Unblocks the read-path trio with zero smolpaws logic change. - Rename the 4 reshaped ones at call sites (
Workspace→LocalWorkspace/RemoteWorkspace,OpenHandsSettings→OpenHandsAgentSettings, swap the workspace guard forinstanceof). Confirm settings field parity. - Decide the 3 deviations in
conversationRuntime.ts: re-wireSecretRegistry→SecretStore; consciously keep-or-drop security + confirmation; portclearRawLlmFieldsWhenProfileSelected. - Flip the ~13 clean imports — path change only, mechanical.
- Shadow-run before cutover: pin both, run one real day (a heartbeat, a WhatsApp media message, a GitHub webhook, one long tool-using session) and diff behavior against 0.10.
src/ + apps/ imports of @smolpaws/agent-sdk (22 symbols, ~21 files) · new package @smolpaws/openhands-agent@0.1.0 src/ exports (170-test transpile). Companion: secrets: keyring vs cipher · agent-sdk#3988.8Decisions & corrections — Aug 30, 2026
A working session that both fixed the SDK and settled several open questions. The map above is the migration surface; this is what we decided to do — and one place the earlier picture was wrong.
apps/slack bridge holds an established connection to the TypeScript agent-server on :8790, which is packages/openhands-agent-server resolving @smolpaws/openhands-agent from its vendored build. So the "paws Slack app on the new transpile" is not a future migration — it is what serves Slack today. The legacy apps/agent-server /turns runner on :8788 is still up but is reference code, unconnected to Slack.Plain text now ends a turn (SDK behavior fix)
The Python SDK finishes a turn when the model replies with plain text (_handle_content_response sets the conversation to FINISHED). The transpile did not: it appended the assistant message and let the run loop continue until finish, stuck, or max-iterations — and it never nudged a reasoning-only reply. Both are now fixed in openhands-agent's dispatchLlmResponse:
- CONTENT → emit the assistant
MessageEvent, then setexecutionStatus = FINISHED. Visible text is a complete turn; control returns to the user. - REASONING_ONLY / EMPTY → emit the message, then append a corrective nudge (
source:"environment", user role) and keep looping.
This makes "last agent message = terminal reply" true by construction, which is exactly what the coordinator's terminalResponseExtractor and the server's getAgentFinalResponse() already assumed. The companion stuck-detector nudge page describes the related asymmetry.
Two ways a message reaches the channel
These are independent, and both are intended — if the model both calls send_message and ends with text, both are delivered:
send_messagetool → a mid-turn outbound. The tool emits a plainActionEvent; the coordinator'ssendMessageExtractordelivers one message per call off the durable EventLog. The tool does not continue-vs-finish — the agent keeps working after it. It belongs directly inopenhands-agentas a first-class built-in tool, and must not embed a delivery callback: queue semantics stay in the coordinator, not the SDK.- Terminal reply → the end-of-turn
finishobservation or plain assistant text, delivered byterminalResponseExtractor.
Feature-by-feature: port / already-there / drop
| Feature | Verdict | Where |
|---|---|---|
send_message | Port as SDK built-in tool (emits ActionEvent) | new: openhands-agent/src/tool/ |
Task scheduling (schedule_task, list_tasks, cancel/pause/resume_task) | Keep. The scheduler (src/task-scheduler.ts) is already SDK-independent (cron + SQLite); only the tool wiring is SDK-coupled. Port the tools like send_message; enqueue turns via coordinator intake, not /turns. | tools: new SDK · scheduler: src/ |
| heartbeat | Keep — host concern, not SDK | launcher / docs/smolpaws/HEARTBEAT.md |
| projectSkills | Already in the new SDK (src/skills/ + AgentContext) | new SDK |
| activity / outbox | Already the coordinator (outboundRelay, deliveryDispatcher, work/projection_cursors) | src/coordinator/ |
| workspace / repo helpers | Drop — VSCode-era leftovers; new SDK's LocalWorkspace/RemoteWorkspace cover real cases | — |
task_tracker ≠ task scheduling
task_tracker tool (a per-conversation plan/checklist, in DEFAULT_EXEC_TOOL_NAMES). That is not smolpaws' cross-conversation cron scheduler (schedule_task et al.). Same word, different job — the scheduler still needs its tools ported; the tracker is already there.Bridges attach to a running core
Target operational shape: the agent-server + coordinator are the always-on core; each bridge (Slack, then WhatsApp, Discord, …) is a lightweight client that attaches to the running core rather than bundling its own server. Today apps/slack runs as a standalone server-plus-bridge; that stays useful for canary/testing, but production is "core up, bridges attach." Note the current bridge base class (BaseBridgeAdapter, used by Discord/email) is welded to the legacy /turns client — the canonical bridge shape is the relay one Slack pioneered, and the others move onto it.
:8790, Slack reconnected. Open follow-ups: port send_message + task tools into the new SDK, and a coordinator bug (smolpaws-6ar) where syncDeliveryOutbox hot-loops 404s on a stale conversation cursor after a server restart.