OpenHands · software-agent-sdk · change synthesis

Two months of the SDK, in one pass

Between late June and late August 2026 the OpenHands/software-agent-sdk repo grew from v1.29.2 to v1.42.1 — fourteen minor releases, 268 commits, roughly 71,000 lines added. This is the overall picture, not a changelog: the themes, the new capabilities, the refactors, and the public surface that moved out from under callers.

Scope · 9663409 (v1.29.2, 2026-06-23) → 1de2e6d (v1.42.1, 2026-08-20)
Repo · OpenHands/software-agent-sdk — 4 packages: openhands-sdk, openhands-agent-server, openhands-tools, openhands-workspace

The through-line: the SDK stopped treating prompts as Jinja files and conversation history as a flat list, and the agent-server grew a real product surface — telemetry, plugins, sub-agents, canvas extensions, and an OpenAI-compatible gateway — while the observable contract was hardened by deprecation deadlines that finally landed.

Scale, by package

+14,555 / −3,524 openhands-sdk

The bulk of the work: prompt registry, conversation tree, LLM provider abstraction, profiles, MCP config, plugin formats, and a security guardrail stack.

+9,956 / −950 agent-server

Mostly additions: telemetry subsystem, plugins/sub-agents/canvas-extension routers, OpenAI gateway, MCP CRUD, and persistence rework.

+461 / −260 · +69 / −97 tools · workspace

Narrower edits: terminal hardening, Gemini edit/write diff sharing, apply_patch error structure, and workspace automation tags.

1 · LLM & providers

The LLM class is where the most surface moved. A LiteLLM-backed provider abstraction (#2363) replaced direct provider dispatch inside llm.py, and a parallel ModelRuntimeMetadata layer now resolves route-aware context/output limits (OpenRouter is the motivating case: the catalog advertises a 1M-token model that a specific endpoint serves at 262k), cached with a TTL and falling back to static metadata on any error.

Before

Provider behavior lived inline in llm.py; mutable extra_headers shared across calls; model limits came only from the static LiteLLM catalog.

After

litellm_provider.py + utils/providers/openrouter.py own provider specifics; extra_headers frozen into an LLMCallContext; runtime metadata resolves per-endpoint limits lazily.

Two new credential concepts arrived. Provider connections (provider_connection_store.py + a server router) hold a shared api_key + optional base_url that many profiles reference by id and resolve at read time, so rotating a key takes effect on the next activation with nothing copied into active settings. And a cleanup profile (clean_outward_text) is a dedicated LLM profile used to polish outward-facing agent text. LLMProfileStore was split into LLMProfileLoader / LLMProfileMutator / LLMProfileStore, and calls were un-serialized from a global config lock. Error handling was consolidated into a typed exceptions/ package plus a closed FailureKind vocabulary (auth / quota / rate_limit / config / transient / agent_action / internal), and subscription validators are now composed rather than bypassed.

2 · Agent loop & prompts

The most visible internal refactor: the system prompt is no longer authored in Jinja. All of the agent/prompts/*.j2 templates were deleted and replaced by a Python prompt registry (context/prompts/presets.py with sections/ — static, dynamic, and a new planning.py ported from the retired system_prompt_planning.j2). Prompt assembly is now programmatic and snapshot-tested rather than template-rendered.

Jinja .j2 files
Deleted: system_prompt.j2, security_policy.j2, in_context_learning_example.j2, system_message_suffix.j2, and every model_specific/* template.
Python registry
context/prompts/presets.py + sections/ compose the system message; planning moves to sections/planning.py.

On the loop itself, the stuck detector now nudges before hard-terminating on a repeating action-error pattern, and the goal loop no longer halts on a STUCK run. Parallel tool execution propagates contextvars per worker thread, stamps structured error classifications, and records observability spans for non-executed (cancelled) tool results. New agent capabilities include structured output (attach a Pydantic response_schema to any tool spec so the model must populate those fields), a vision-inspect helper tool that lets non-vision models ask a saved vision profile about an image, and a canonical default_tool_specs that lets the SDK own default tool names as data without importing openhands-tools.

3 · Tools

Tool changes are comparatively contained. The terminal gained client-configurable session env, Windows hardening (multiline PowerShell submission, Ctrl-C cleanup, send-keys), and secret masking for all registered secrets — not just exported ones. apply_patch replaced silent assert failures with structured DiffError raises and fixed a race. The Gemini edit/write_file tools now share one diff renderer via a new file_change.py, and file_editor preserves whitespace in the str_replace fallback. Delegation got a visualizer overhaul and cleanup fixes, and the task manager was reworked.

4 · Workspace & git

The workspace layer grew an automation surface: a base workspace now derives automation tags, and a completion callback POSTs conversation status (with accumulated LLM cost) to AUTOMATION_CALLBACK_URL, carrying a structured ConversationErrorEvent on failure. Cloud workspace followed with automation tags and SDK settings passthrough. On the git side, a new git_commits.py adds a commit-history API (list commits + per-commit diffs rendered from git objects, so deleted files still render), and the agent-server exposes a workspace archive endpoint for git-delta / tar.gz plus /git/commits.

5 · Conversation, events & persistence

This is the other structural headline: conversation history became a tree. Events carry a parent_id, state tracks a movable leaf_event_id HEAD (with a reserved ROOT_PARENT_ID for deliberately re-rooted trees), and the SDK + server expose fork-from-event, navigate, and lineage over both local state and HTTP. A lazy-hydration pass makes persisted conversations load on demand, idle conversations are evicted after a configurable TTL, and parent/child conversation relationships are now persisted.

Events gained a privacy-safe failure contract (ErrorClassification, FailureKind) that crosses the event/API boundary, and a corrective nudge is now emitted as an environment event. New opt-in persistent memory reads two tiers of agent-maintained MEMORY.md indexes (user ~/.openhands/memory/ and project .openhands/memory/) into prompt-ready text.

6 · Settings & profiles

Profiles became store-agnostic (a protocol + hoisted router helpers as a cloud prereq), gained a seed profile and a resolvable default LLM profile backfilled at seed time, and a public from_persisted() entry point that loads persisted settings through migrations. Skill selection flipped from an allow-list to a deny-list: skill_refs is replaced by disabled_skills. MCP grew a settings-backed auth credential store, an enabled flag to switch a server off without removing it, and OAuth token persistence via a FastMCP AsyncKeyValue adapter.

Before

skill_refs allow-list selected skills; MCP servers had no per-server off switch or settings-backed OAuth.

After

disabled_skills deny-list (whole catalog on by default); MCPServer.enabled; MCP OAuth tokens live in settings with the same encryption/redaction path.

7 · Agent-server

The server gained the most new surface, almost entirely additive. A new telemetry subsystem emits an allowlisted set of lifecycle/failure events (never prompts, messages, paths, secrets, or bodies) through a split-consent policy to PostHog or HTTP exporters, with pseudonymization and a DO_NOT_TRACK kill switch. Canvas extensions got a manifest, containment, installation persistence, and a staged check/apply refresh. Plugins grew a strategy-based format loader (root plugin.json closed schema + Claude Code format), installed/local auto-load, and full CRUD routers. Sub-agents got a discovery endpoint mirroring discover_agents.

new subsystem

Telemetry

telemetry/ (service, policy, sanitizer, sink, subscriber, exporters) — off by default, consent-gated, pseudonymized.

new routers

Surface

plugins_router, sub_agents_router, provider_connections_router, openai/router.py, MCP settings CRUD.

protocol

OpenAI gateway

/v1/models + chat completions over the same session key — call the agent-server through the OpenAI protocol.

Persistence was reworked around base_state.json as the single source of truth for the agent, ending the meta.json duplication that had reverted restored conversations' LLM config and doubled a trajectory secret leak. The REST API is now published as a typed OpenAPI contract with breakage checks, secrets are redacted at more boundaries, the default bind host moved to loopback without a session key, and WebSocket auth happens outside URLs.

8 · Security

The security stack deepened in two directions. ToolShield (#2911) adds an LLM-as-guardrail analyzer that issues a separate guardrail completion evaluating each proposed action against recent history — distinct from the actor LLM annotating its own risk — with optional per-tool "safety experiences". And the pattern analyzer got an AST-backed shell-command resolver (tree-sitter shell_semantics.py) so quoted, path-qualified, and nested command names become visible to the force-delete detector, reporting UNKNOWN (fails-safe) when it cannot vouch for what it saw. A secret-disclosure consent rule joined the agent security policy.

Breaking surface

The deprecation pipeline finally cut. These public names are gone or on a deadline:

Removed at 1.41.0StartACPConversationRequest (use StartConversationRequest), AgentDefinition.mcp_servers (use mcp_config), AgentProfileDiagnostics.resolved_mcp_servers (use resolved_mcp_config_keys).
Deprecated 1.40.0 → removed 1.45.0AgentBase.model_dump_succint (use model_dump(exclude_none=True)).
Renamed/reshapedskill_refsdisabled_skills; Jinja prompt templates removed (internal, but downstream renderers that read them are broken); extra_headers → frozen LLMCallContext.

Sources of truth