SDK migration map · smolpaws

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.

13 · drop-in AgentContext · Event · MessageEvent · TextContent Message · ToolDefinition · Skill · loadSkillsFromDir BrowserTool · FileStore · LocalConversation … 4 · renamed Workspace → OpenHandsSettings → AgentServerWorkspace … 3 · port event guards reduceText- Content 3 · decide security confirm policy SecretRegistry
The swap is mostly green. The real work is the 6 amber/red symbols — and 4 of those live in one file.

2Clean map — drop-in 13

Same name, same shape, exported from the new package barrel. Change the import path, done.

SymbolNew package locationsmolpaws files
AgentContextcontext/agent-context.ts:22conversationRuntime
BrowserTooltools/index.ts:217conversationRuntime
Event typeevent/index.ts:248eventRouter, sockets, eventService
FileStoreio/index.ts:8conversationService
LocalConversationconversation/local-conversation.ts:14conversationRuntime
Skillskills/index.ts:38projectSkills
TextContentllm/index.ts:146conversationRouter, eventRouter, sockets, turnState
Message typellm/index.ts:150(type re-export)
ToolDefinitiontool/index.ts:58outboundMessaging, taskCommands
MessageEvent typeevent/index.ts:257(type re-export)
loadSkillsFromDirskills/index.ts:117projectSkills

(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)Notesmolpaws files
WorkspaceBaseWorkspace / LocalWorkspace workspace:32,48base is now an interface; pick LocalWorkspace or RemoteWorkspaceconversationRuntime, shared-runner
AgentServerWorkspaceRemoteWorkspace workspace:125the agent-server-backed workspace, renamed(type)
isAgentServerWorkspace— (use instanceof RemoteWorkspace)guard not exported; trivially replacedshared-runner
OpenHandsSettingsOpenHandsAgentSettings 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.

SymbolWhat it doessmolpaws files
isMessageEventevent type guard — the new pkg has isAcpPatchEdit but not this familyactivityRouter, conversationState
isConversationStateUpdateEventevent type guardconversationState
reduceTextContentcollapses TextContent[] → string; used all over the read pathactivityRouter, conversationState, messageText
All three are tiny, dependency-free functions. Cheapest path: port them into the new package's 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?"

SymbolWasDecisionsmolpaws files
LLMSecurityAnalyzerguardrail LLM / risk scoringDropped by design. Do we rely on it? If yes → keep as a smolpaws-side wrapper; if no → delete the wiring.conversationRuntime
createConfirmationPolicyFromSettingsconfirmation gate before risky actionsDropped by design. This is the "don't do irreversible things unsupervised" lever — decide consciously (see caution).conversationRuntime
SecretRegistryPython secret-split registryReplaced by the new SecretStore / keyring model (see the secrets page). Re-wire, don't port.conversationRuntime
clearRawLlmFieldsWhenProfileSelectedsettings hygiene helperNot in new pkg. Small — port it, or fold into the profile-selection path.conversationRuntime
Caution — the confirmation/security pair. These are exactly the kind of thing you don't miss until an agent does something irreversible unsupervised. Dropping them is defensible for a trusted local cat, but it should be a chosen tradeoff, not a silent consequence of the swap. Confirm smolpaws doesn't lean on them before deleting the wiring.

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.

conversationRuntime.ts 8 symbols · all 3 deviations + 2 renames → the file to migrate first & carefully read-path trio conversationState · activityRouter event guards + reduceTextContent the long tail (~17 files) TextContent, Event, ToolDefinition, Skill… → path-only import changes
Migration difficulty is not spread evenly — it's concentrated. Fix one file well and most of the risk is gone.

7Suggested migration order

  1. Port the 3 pure helpers (isMessageEvent, isConversationStateUpdateEvent, reduceTextContent) into the new package & export them. Unblocks the read-path trio with zero smolpaws logic change.
  2. Rename the 4 reshaped ones at call sites (WorkspaceLocalWorkspace/RemoteWorkspace, OpenHandsSettingsOpenHandsAgentSettings, swap the workspace guard for instanceof). Confirm settings field parity.
  3. Decide the 3 deviations in conversationRuntime.ts: re-wire SecretRegistrySecretStore; consciously keep-or-drop security + confirmation; port clearRawLlmFieldsWhenProfileSelected.
  4. Flip the ~13 clean imports — path change only, mechanical.
  5. 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.
Bottom line: the swap is real but bounded. 13 are free, 4 are renames, 3 are tiny ports, and 3 are decisions — and the decisions all live in one file. This is a hookup, not a rewrite.
Grounded to: smolpaws 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.

★ The running Slack path already uses the new SDK. Traced from live processes, not docs: the 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 set executionStatus = 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_message tool → a mid-turn outbound. The tool emits a plain ActionEvent; the coordinator's sendMessageExtractor delivers one message per call off the durable EventLog. The tool does not continue-vs-finish — the agent keeps working after it. It belongs directly in openhands-agent as 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 finish observation or plain assistant text, delivered by terminalResponseExtractor.

Feature-by-feature: port / already-there / drop

FeatureVerdictWhere
send_messagePort 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/
heartbeatKeep — host concern, not SDKlauncher / docs/smolpaws/HEARTBEAT.md
projectSkillsAlready in the new SDK (src/skills/ + AgentContext)new SDK
activity / outboxAlready the coordinator (outboundRelay, deliveryDispatcher, work/projection_cursors)src/coordinator/
workspace / repo helpersDrop — VSCode-era leftovers; new SDK's LocalWorkspace/RemoteWorkspace cover real cases

task_tracker ≠ task scheduling

The new SDK ships a built-in 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.

State on Aug 30: SDK fix landed (content-terminal + reasoning nudge), rebuilt & re-vendored into :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.