Skip to content

feat(sdk): split AgentSettings into LLM/ACP discriminated union#2861

Merged
simonrosenberg merged 7 commits intomainfrom
feat/acp-discriminated-union-v2
Apr 17, 2026
Merged

feat(sdk): split AgentSettings into LLM/ACP discriminated union#2861
simonrosenberg merged 7 commits intomainfrom
feat/acp-discriminated-union-v2

Conversation

@simonrosenberg
Copy link
Copy Markdown
Collaborator

@simonrosenberg simonrosenberg commented Apr 16, 2026

Summary

Closes #2853.

Replaces the monolithic AgentSettings — which silently ignored llm, tools, mcp_config, condenser, and verification when acp_command was set — with a discriminated union over two concrete variants.

  • LLMAgentSettings: the long-standing LLM-agent shape (agent, llm, tools, mcp_config, agent_context, condenser, verification).
  • ACPAgentSettings: ACP-delegating subprocess agent. Keeps llm (for cost/token metrics attribution — the ACP subprocess does its own completions, but we still need a model id for pricing and telemetry) plus the ACP-specific fields: acp_command, acp_args, acp_env, acp_model, acp_session_mode, acp_prompt_timeout.
  • AgentSettings is now a TypeAlias: Annotated[LLMAgentSettings | ACPAgentSettings, Discriminator(...)]. The discriminator defaults missing agent_kind"llm" so existing persisted payloads round-trip without a migration.
  • create_agent() lives on each variant with a narrowed return type (Agent vs ACPAgent) — no more AgentBase fallback.
  • export_agent_settings_schema() produces a single schema with a synthetic agent_kind critical choice field in general and sections tagged with a new variant field ("llm" / "acp" / None) so GUIs can show only the sections that apply to the active variant.
  • New helpers: validate_agent_settings(data), default_agent_settings(), create_agent_from_settings(settings).

Note on the issue text

The issue described llm as "silently-ignored" in ACP mode. In practice, ACPAgent does accept an llm field — it's a dummy "acp-managed" placeholder by default, but gets used for cost/token attribution (llm.metrics.model_name is rewritten to acp_model in model_post_init). So the redesign here keeps llm on ACPAgentSettings rather than dropping it outright as Option 1 in the issue suggested. The LLM-only fields (tools, mcp_config, agent_context, condenser, verification) that ACPAgent.init_state() already rejects are the ones actually removed from the ACP variant.

Breaking change

Direct construction AgentSettings(...) no longer works — callers must pick LLMAgentSettings(...) or ACPAgentSettings(...). The OpenHands repo companion PR demonstrates the migration (tests use from openhands.sdk.settings import LLMAgentSettings as AgentSettings as a temporary alias).

Test plan

  • 21 new/updated tests in tests/sdk/test_settings.py (per-variant schema export, discriminated-union validation including missing-discriminator default, create_agent() narrowing, ConversationSettings.create_request for both request types).
  • tests/agent_server/test_settings_router.pyAgentSettings schema endpoint still serves a schema named "AgentSettings" with llm/condenser/verification sections (now variant-tagged).
  • Full SDK + agent-server suite: 3188 passing.

🤖 Generated with Claude Code


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:f13a4ba-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-f13a4ba-python \
  ghcr.io/openhands/agent-server:f13a4ba-python

All tags pushed for this build

ghcr.io/openhands/agent-server:f13a4ba-golang-amd64
ghcr.io/openhands/agent-server:f13a4ba-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:f13a4ba-golang-arm64
ghcr.io/openhands/agent-server:f13a4ba-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:f13a4ba-java-amd64
ghcr.io/openhands/agent-server:f13a4ba-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:f13a4ba-java-arm64
ghcr.io/openhands/agent-server:f13a4ba-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:f13a4ba-python-amd64
ghcr.io/openhands/agent-server:f13a4ba-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:f13a4ba-python-arm64
ghcr.io/openhands/agent-server:f13a4ba-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:f13a4ba-golang
ghcr.io/openhands/agent-server:f13a4ba-java
ghcr.io/openhands/agent-server:f13a4ba-python

About Multi-Architecture Support

  • Each variant tag (e.g., f13a4ba-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., f13a4ba-python-amd64) are also available if needed

Replaces the monolithic ``AgentSettings`` (which silently ignored
``llm``/``tools``/``mcp_config``/``condenser``/``verification`` when
``acp_command`` was set) with a discriminated union over two concrete
variants:

- ``LLMAgentSettings`` — the long-standing LLM-agent shape.
- ``ACPAgentSettings`` — ACP subprocess agent; keeps ``llm`` (for
  cost/token metrics attribution) and owns the ACP-specific fields
  (``acp_command``/``acp_args``/``acp_env``/``acp_model``/
  ``acp_session_mode``/``acp_prompt_timeout``).

``AgentSettings`` is now a TypeAlias using ``Annotated[... ,
Discriminator(...)]`` with a discriminator that defaults missing
``agent_kind`` to ``"llm"`` so existing persisted payloads
round-trip without migration. ``create_agent()`` lives on each
variant with a narrowed return type (``Agent`` vs ``ACPAgent``) —
no more ``AgentBase`` fallback or silently-ignored fields.

Adds ``export_agent_settings_schema()`` which produces a single
schema with a synthetic ``agent_kind`` critical choice field in
``general`` and sections tagged with a new ``variant`` field
(``"llm"`` / ``"acp"`` / ``None``) so GUIs can show only the
sections that apply to the selected variant.

New helpers: ``validate_agent_settings(data)``, ``default_agent_settings()``,
and ``create_agent_from_settings(settings)``.

Closes #2853
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Solid discriminated union refactor that properly models two distinct agent types. The backward-compatible discriminator and comprehensive tests reduce risk. A few areas could be clearer but the trade-offs are reasonable.

Comment thread openhands-sdk/openhands/sdk/settings/model.py
Comment thread openhands-sdk/openhands/sdk/settings/model.py
Comment thread openhands-sdk/openhands/sdk/settings/model.py
Comment thread openhands-sdk/openhands/sdk/settings/model.py Outdated
Comment thread tests/sdk/test_settings.py
Comment thread openhands-sdk/openhands/sdk/settings/model.py
@all-hands-bot
Copy link
Copy Markdown
Collaborator

Risk Assessment

🟡 MEDIUM

Key Factors:

  • Breaking API change: Direct AgentSettings(...) construction no longer works. Callers must use LLMAgentSettings(...) or ACPAgentSettings(...).
  • Backward-compatible deserialization: Old persisted payloads (missing agent_kind) default to "llm" and validate correctly.
  • Foundational refactor: Affects core agent configuration surface used throughout the SDK.
  • Companion PR required: OpenHands repo needs updates (mentioned in PR description).
  • Comprehensive tests: 21 new/updated tests cover discriminator, schema export, agent creation, and round-tripping for both variants.
  • No behavior changes: This is an API refactor; agent logic and prompts are unchanged.

Benchmark Impact:
Should NOT affect benchmarks - No changes to agent behavior, prompts, or tool execution. This is purely a settings API refactor.

Recommendation:
Safe to merge after companion PR is ready - The discriminated union properly models two distinct agent types and eliminates the previous hack where ACP agents silently ignored LLM-specific fields. The breaking change is intentional and well-documented.


Overall Verdict: The refactor is architecturally sound and solves a real problem. The schema merge complexity and semantic oddity of ACPAgentSettings.llm are acceptable trade-offs given the constraints.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 16, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk
   __init__.py27292%102–103
openhands-sdk/openhands/sdk/settings
   model.py4163791%263, 265, 277, 279, 412, 425, 436, 446, 450, 452, 454, 456, 458, 460, 462, 669, 671, 867, 903, 1002, 1038–1041, 1067, 1191, 1236, 1268, 1278, 1280, 1285, 1303, 1316, 1318, 1320, 1322, 1329
TOTAL23314568775% 

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ QA Report: PASS

The discriminated union successfully replaces monolithic AgentSettings, provides type-safe variant selection, and maintains backward compatibility.

Does this PR achieve its stated goal?

Yes. This PR delivers exactly what it promises: a discriminated union over LLMAgentSettings and ACPAgentSettings that eliminates silent field ignoring, provides narrowed return types from create_agent(), exports a schema with variant tags for GUI filtering, and maintains backward compatibility for existing persisted payloads that lack the agent_kind discriminator.

Phase Result
Environment Setup ✅ Build successful, dependencies installed
CI & Tests ✅ sdk-tests pass (2m48s), agent-server-tests pass (46s), 21 new tests pass
Functional Verification ✅ All 8 behavioral scenarios verified end-to-end
Functional Verification

Test 1: Discriminator defaults to 'llm' for backward compatibility

Step 1 — Establish baseline (old payload without agent_kind):

Created a payload simulating persisted AgentSettings from before this PR:

old_payload = {
    "schema_version": 1,
    "llm": {"model": "test-model"},
    # No agent_kind field
}

Step 2 — Validate the payload:

Ran validate_agent_settings(old_payload):

Type: LLMAgentSettings
agent_kind: llm

This confirms the discriminator function correctly defaults missing agent_kind to 'llm', preserving backward compatibility for existing persisted settings.

Step 3 — Round-trip and verify agent_kind is added:

Serialized the validated settings back:

roundtrip = validated.model_dump(mode="json")
print(roundtrip["agent_kind"])  # Output: llm

The round-trip adds the agent_kind field, so future saves will include it explicitly.


Test 2: Explicit agent_kind='llm' creates LLMAgentSettings

Validated payload:

llm_payload = {
    "agent_kind": "llm",
    "llm": {"model": "claude-3-5-sonnet"},
}
validated = validate_agent_settings(llm_payload)

Result:

Type: LLMAgentSettings
Model: claude-3-5-sonnet

Confirms explicit agent_kind='llm' dispatches to the LLM variant.


Test 3: Explicit agent_kind='acp' creates ACPAgentSettings

Validated payload:

acp_payload = {
    "agent_kind": "acp",
    "acp_command": ["npx", "-y", "claude-agent-acp"],
    "acp_model": "claude-opus-4-6",
}
validated = validate_agent_settings(acp_payload)

Result:

Type: ACPAgentSettings
Command: ['npx', '-y', 'claude-agent-acp']
Model: claude-opus-4-6

Confirms explicit agent_kind='acp' dispatches to the ACP variant.


Test 4: LLMAgentSettings.create_agent() returns narrowed Agent type

Created settings and agent:

settings = LLMAgentSettings(llm=LLM(model="test-model", api_key="fake-key"))
agent = settings.create_agent()

Result:

Type: Agent

Confirms the LLM variant's create_agent() returns the narrowed Agent type (not AgentBase).


Test 5: ACPAgentSettings.create_agent() returns narrowed ACPAgent type

Created settings and agent:

settings = ACPAgentSettings(acp_command=["echo", "test"], acp_model="claude-opus-4-6")
agent = settings.create_agent()

Result:

Type: ACPAgent
acp_command: ['echo', 'test']
acp_model: claude-opus-4-6

Confirms the ACP variant's create_agent() returns the narrowed ACPAgent type.


Test 6: Schema export includes variant-tagged sections

Exported schema:

schema = export_agent_settings_schema()
print(f"model_name: {schema.model_name}")  # Output: AgentSettings

Variant tags found:

Sections with variants:
  general (variant=None)
  llm (variant=llm)
  condenser (variant=llm)
  verification (variant=llm)
  acp (variant=acp)
  llm (variant=acp)  # LLM section for metrics

Synthetic agent_kind field:

general_section = next(s for s in schema.sections if s.key == "general")
agent_kind_field = next(f for f in general_section.fields if f.key == "agent_kind")
print(f"Choices: {[c.value for c in agent_kind_field.choices]}")
# Output: ['llm', 'acp']

This confirms the schema export adds a synthetic agent_kind selector field and tags sections with their applicable variant so GUIs can filter by the active agent type.


Test 7: ACP variant includes llm field for metrics, excludes LLM-only fields

Created ACP settings:

acp = ACPAgentSettings(
    acp_command=["npx", "-y", "claude-agent-acp"],
    acp_model="claude-opus-4-6",
    llm=LLM(model="claude-opus-4-6", api_key="test-key")
)

LLM field is present:

llm.model: claude-opus-4-6

As documented in the PR description, the ACP variant keeps llm for cost/token attribution even though the ACP subprocess makes its own model calls.

LLM-only fields are absent:

acp_payload = acp.model_dump()
assert "tools" not in acp_payload
assert "mcp_config" not in acp_payload
assert "agent_context" not in acp_payload
assert "condenser" not in acp_payload
assert "verification" not in acp_payload

All assertions pass, confirming the fields that ACPAgent.init_state() rejects are correctly excluded from the ACP variant.


Test 8: Breaking change — old AgentSettings() constructor fails

Attempted direct construction:

from openhands.sdk import AgentSettings
settings = AgentSettings(llm=LLM(model="test"))

Result:

TypeError: Cannot instantiate typing.Union

Confirms the documented breaking change: direct AgentSettings() construction no longer works because AgentSettings is now a TypeAlias for a discriminated union. Callers must use LLMAgentSettings() or ACPAgentSettings() explicitly, or use validate_agent_settings() / default_agent_settings() helpers.


Test 9: Updated example runs successfully

Ran examples/01_standalone_sdk/46_agent_settings.py:

✓ Roundtrip deserialization successful — all fields preserved
Agent created: llm.model=litellm_proxy/claude-sonnet-4-5-20250929
  tools=['terminal', 'file_editor']
  condenser=LLMSummarizingCondenser

The example was updated to use LLMAgentSettings and runs without errors, demonstrating the migration path for existing code.

Issues Found

None. The pre-commit formatting issue (ruff-format wanted to split a long generator expression) has been fixed.

Comment thread openhands-sdk/openhands/sdk/settings/model.py Outdated
Debug Agent added 2 commits April 16, 2026 16:41
…t_kind

UX iteration on the discriminated union — the raw ``acp_command`` JSON
array and the stray ``agent_kind`` / ``CodeActAgent`` fields were
confusing on the ACP settings page. Three changes:

1. ``acp_server`` CRITICAL choice field (``claude-code`` / ``codex`` /
   ``gemini-cli`` / ``custom``) drives the subprocess command. The
   raw ``acp_command`` is demoted to MINOR and is only needed when
   ``acp_server='custom'`` (or when overriding the default install
   path). ``ACPAgentSettings.resolve_acp_command()`` maps known
   servers to their npx defaults (e.g. ``claude-code`` →
   ``npx -y @agentclientprotocol/claude-agent-acp``).

2. Field-level ``variant`` on ``SettingsFieldMetadata`` +
   ``SettingsFieldSchema``. The LLM-only general fields (``agent``,
   ``tools``, ``mcp_config``) now carry ``variant='llm'`` so the GUI
   can hide them on the ACP settings page. Nested fields inherit
   their section's variant.

3. ``export_agent_settings_schema()`` no longer emits a synthetic
   ``agent_kind`` choice field. Each variant lives on its own GUI
   settings page and that page injects ``agent_kind`` on save — the
   sidebar-route is the discriminator. Simpler than asking users to
   set both.

``acp_model`` is also promoted to CRITICAL (it's the second thing
users want to set after picking the server).
…AgentSettingsConfig

Addresses the Python-API-breakage CI check on #2861, which flagged the
12 ``AgentSettings.*`` members (``schema_version``, ``agent``, ``llm``,
``tools``, ``mcp_config``, ``agent_context``, ``condenser``,
``verification``, ``export_schema``, ``create_agent``,
``build_condenser``, ``build_critic``) as removed without deprecation.

Instead of making ``AgentSettings`` a TypeAlias (which strips its
attribute surface), keep it as a concrete class and make it a
:class:`LLMAgentSettings` subclass. Every v1.17.0 attribute and method
resolves through inheritance, so griffe sees them as preserved. The
class body calls ``warn_deprecated("AgentSettings", deprecated_in="1.17.0",
removed_in="1.22.0", …)`` in ``__init__`` to emit a runtime warning
and mark the name as deprecated in the AST so the CI policy is
satisfied.

The discriminated-union TypeAlias moves to the new name
``AgentSettingsConfig``. Fields that previously read

    agent_settings: AgentSettings | None

now read

    agent_settings: AgentSettingsConfig | None

and the Pydantic discriminator resolves either variant on validation.
Downstream callers should:

- use ``LLMAgentSettings`` / ``ACPAgentSettings`` for explicit variants,
- use ``AgentSettingsConfig`` as the type for fields accepting either,
- use ``validate_agent_settings(...)`` to validate raw payloads.

``AgentSettings(...)`` construction continues to work for the v1.17
compatibility window (through v1.22).
simonrosenberg pushed a commit to simonrosenberg/OpenHands that referenced this pull request Apr 16, 2026
The SDK companion PR (OpenHands/software-agent-sdk#2861) keeps
``AgentSettings`` as a deprecated v1.17-compat class and renames the
discriminated-union TypeAlias to ``AgentSettingsConfig``. Update the
downstream type annotation accordingly so the ``agent_settings`` field
continues to accept either the LLM or ACP variant at validation time.

- ``Settings.agent_settings: AgentSettingsConfig`` (was ``AgentSettings``).
- ``Settings.to_agent_settings`` return type narrows to the explicit
  ``LLMAgentSettings | ACPAgentSettings`` union.
- ``agent_settings_serializer`` likewise.
Debug Agent added 2 commits April 16, 2026 17:37
…essage

CI's ``pre-commit`` job flagged two ruff-format fixes:

- ``from __future__ import annotations`` is already in effect, so
  ``agent_settings: "AgentSettingsConfig | None"`` no longer needs the
  forward-reference string.
- A long assertion message is reflowed across three lines per
  ruff-format's line-length policy.

No behavior changes.
…ault

``@google/gemini-cli`` 0.38+ renamed the ACP activation flag from
``--experimental-acp`` to ``--acp`` (the old flag still works, with a
deprecation notice). Update the default ``acp_server='gemini-cli'``
command so fresh users hit the supported path.

Verified by probing each ACP server's ``initialize`` response:

- claude-agent-acp 0.29.0 — no ACP-level auth (uses env vars directly)
- codex-acp 0.9.4 — auth methods: chatgpt / codex-api-key / openai-api-key
- gemini-cli 0.38.1 — auth methods: oauth-personal / gemini-api-key /
  vertex-ai / gateway (the "gateway" method is what the SDK's
  ``_select_auth_method`` targets with ``GEMINI_BASE_URL``)
The frontend's ``depends_on`` filter evaluates each dependency as a
boolean (``values[dep] === true``). That works for real booleans
(e.g. ``condenser.enabled``) but evaluates to false for the
string-valued ``acp_server``, hiding ``acp_command`` outright — even
in the "all" view of the ACP Server page — so the ``acp_server=custom``
workflow was unreachable from the GUI.

Drop the bogus metadata and rely on prominence + view filtering. The
field remains MINOR (visible in the "all" view). No behavior change
for CLI / API callers.
@enyst
Copy link
Copy Markdown
Collaborator

enyst commented Apr 17, 2026

@OpenHands /codereview this PR. Provide feedback as a review with gh api

Then verify what exactly changes for existing clients of the SDK via Python API and REST API, and if our two CI workflows for API breakages succeeded, wdyt, is it correct that they should succeed or do you think we should make them more stringent and why?

@openhands-ai
Copy link
Copy Markdown

openhands-ai bot commented Apr 17, 2026

I'm on it! enyst can track my progress at all-hands.dev

Copy link
Copy Markdown
Collaborator

@enyst enyst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable

[IMPROVEMENT OPPORTUNITIES]

  • [PR description] The body is stale after the compatibility follow-up: it still says direct AgentSettings(...) construction no longer works, but the shipped code keeps AgentSettings as a deprecated LLMAgentSettings subclass. I would update the PR description before merge so reviewers and downstream users get the real migration story.
  • [REST contract: /api/settings/agent-schema] The OpenAPI diff is additive only, so the REST breakage workflow passing is mechanically correct today. But the runtime payload semantics did change in a way OpenAPI does not capture: the endpoint now returns variant-tagged sections and duplicate logical section keys (for example llm exists for both the llm and acp variants). Any client indexing sections by section.key alone could break even though oasdiff stays green.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟡 MEDIUM
    The Python API compatibility story is good: existing AgentSettings callers keep working and get a deprecation runway, which is exactly what the SDK breakage policy wants. The remaining risk is on the schema-serving REST endpoint, where the wire schema is still compatible but the returned schema content now has new semantics that current CI does not model.

VERDICT:
Worth merging: core logic is sound, the deprecation shim is the right way to preserve Python userspace, and the main follow-up is tightening how we review/schema-check the settings endpoint semantics.

KEY INSIGHT:
The important compatibility win here is that Python userspace is preserved; the subtle compatibility risk lives in schema endpoint semantics, not in the exported Python symbols.

This review was generated by an AI assistant (OpenHands) on behalf of enyst.

@openhands-ai
Copy link
Copy Markdown

openhands-ai bot commented Apr 17, 2026

Done.

  • Reviewed PR feat(sdk): split AgentSettings into LLM/ACP discriminated union #2861 and submitted a GitHub review via gh api:
  • Assessed Python SDK client impact:
    • AgentSettings(...) still works via a deprecated LLMAgentSettings subclass.
    • No public SDK exports were removed; only additive exports were introduced.
    • New union-aware callers should use AgentSettingsConfig / validate_agent_settings(...).
    • Noted one subtle change: AgentSettings is no longer the “either variant” type.
  • Assessed REST API client impact:
    • OpenAPI-compatible/additive at the schema level.
    • But /api/settings/agent-schema changed semantically: combined variant-aware content, variant tags, and potentially duplicate logical section keys.
  • Evaluated the two API-breakage CI workflows:
    • Python API breakage workflow passing is correct.
    • REST API breakage workflow passing is also correct under current rules, but not sufficient to catch semantic changes in settings-schema payloads.
  • Provided recommendation:
    • Keep the Python API check as-is.
    • Make REST/settings-schema compatibility checks more stringent with payload snapshot or semantic invariant checks.
  • Flagged one follow-up issue in review:
    • The PR description is stale because it still says AgentSettings(...) no longer works, while the implementation now preserves it with deprecation.

Conciseness:

  • No code changes were made.
  • No extraneous repository changes were introduced.

@simonrosenberg simonrosenberg merged commit 9158a1d into main Apr 17, 2026
34 of 35 checks passed
@simonrosenberg simonrosenberg deleted the feat/acp-discriminated-union-v2 branch April 17, 2026 19:37
simonrosenberg pushed a commit to simonrosenberg/OpenHands that referenced this pull request Apr 17, 2026
Integration layer for ACP-in-GUI, building on the discriminated-union
AgentSettings merged as OpenHands/software-agent-sdk#2861.

When ``AgentSettings`` is an ``ACPAgentSettings`` the app server now:

- persists ``agent_kind='acp'`` on ``AppConversationInfo`` (new column
  via Alembic ``009``; existing rows coerce NULL → ``'llm'``)
- POSTs conversation-start to ``/api/acp/conversations`` on the
  agent-server, skipping LLM-only concerns (skills, hooks, server
  overrides)
- plumbs UI-saved credentials from ``llm.api_key`` / ``llm.base_url``
  into the subprocess env as provider-native vars
  (``ANTHROPIC_API_KEY`` etc.), gated on ``api_key`` being set so a
  provider-default ``base_url`` alone never clobbers an explicit
  ``OH_AGENT_SERVER_ENV`` passthrough
- branches ``conversation_url`` and live-status polling to the right
  router so clients don't 404 on ``/api/conversations/{id}``

Also redirects the agent-server subprocess stdout/stderr to a log file
at ``{working_dir}/.openhands-agent-server.log`` — the previous
``PIPE`` approach deadlocked once the 64 KB kernel buffer filled
without a drain loop.

This is the backend half of OpenHands#13983; the settings UX lives in a
follow-up PR to keep this one small and reviewable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@enyst
Copy link
Copy Markdown
Collaborator

enyst commented Apr 17, 2026

[REST contract: /api/settings/agent-schema] The OpenAPI diff is additive only, so the REST breakage workflow passing is mechanically correct today. But the runtime payload semantics did change in a way OpenAPI does not capture: the endpoint now returns variant-tagged sections and duplicate logical section keys (for example llm exists for both the llm and acp variants). Any client indexing sections by section.key alone could break even though oasdiff stays green.

@simonrosenberg Just wondering, so we know if we need to fix anything. Is this going to affect CLI or Web clients?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AgentSettings: move ACP to a discriminated union (or sibling model) to eliminate silently-ignored fields

3 participants