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
The bulk of the work: prompt registry, conversation tree, LLM provider abstraction, profiles, MCP config, plugin formats, and a security guardrail stack.
Mostly additions: telemetry subsystem, plugins/sub-agents/canvas-extension routers, OpenAI gateway, MCP CRUD, and persistence rework.
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.
system_prompt.j2, security_policy.j2, in_context_learning_example.j2, system_message_suffix.j2, and every model_specific/* template.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.
Telemetry
telemetry/ (service, policy, sanitizer, sink, subscriber, exporters) — off by default, consent-gated, pseudonymized.
Surface
plugins_router, sub_agents_router, provider_connections_router, openai/router.py, MCP settings CRUD.
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:
StartACPConversationRequest (use StartConversationRequest), AgentDefinition.mcp_servers (use mcp_config), AgentProfileDiagnostics.resolved_mcp_servers (use resolved_mcp_config_keys).AgentBase.model_dump_succint (use model_dump(exclude_none=True)).skill_refs → disabled_skills; Jinja prompt templates removed (internal, but downstream renderers that read them are broken); extra_headers → frozen LLMCallContext.Sources of truth
- GitHub compare of the two SHAs
- OpenHands/software-agent-sdk —
git log --oneline 9663409..1de2e6d,git diff --stat, and per-file diffs read directly