mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 17:08:34 +08:00
* feat(agent-profiles): minimal local Agent Profiles library reusing the Agent settings form Adds a Settings → Agent profiles library (local backends only) that mirrors the LLM-profiles UX: a list of named profiles with a create/edit view that reuses the existing Agent settings form as the editor — you just add a name (and, for OpenHands agents, pick an LLM profile). Deliberately minimal vs the full Phase-4 UX: no chat-input picker, no live switch, no Settings information-architecture rework. Condenser / verification / MCP stay global, exactly as on main. - Data layer: AgentProfilesService + list/save/delete/rename/activate hooks wrapping the ts-client AgentProfilesClient (endpoints shipped in agent-server v1.29.0). - Editor: AgentSettingsScreen gains an opt-in `embedded` mode (hides its header + global Save, seeds from an override, and reports state via a save control) — mirroring how LlmSettingsScreen is embedded in the LLM-profiles view. The global Agent settings page is unchanged. - Library: AgentProfilesLocalView (list/create/edit) + manager/body/row/menu + delete modal, at the additive route /settings/agents, gated to local backends (cloud has no /api/agent-profiles surface yet, epic #3730). - Maps the form to AgentProfileSaveInput: OpenHands requires an llm_profile_ref (via a picker); ACP stores acp_server/acp_model and the command as a shell string. Validated end-to-end against a real agent-server. Part of OpenHands/software-agent-sdk#3713 (Phase 4). An alternative to the larger #1550. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-profiles): add chat-input agent-profile picker + live in-conversation switch Adds the full chat integration for Agent Profiles (epic #3713, #3727), keeping the simplified library/editor from the previous commit: - New-conversation picker (home): an agent-profile toggle replaces the LLM- profile toggle. Selecting activates the profile so the next conversation launches from it; conversations start via `agent_profile_id` (resolved server-side) instead of an inline agent_settings dump. - Mid-conversation switch, capability-gated by the running agent: - OpenHands conversation → live LLM-profile switch (`/switch_profile`). - ACP conversation → live model switch (`set_session_model`, existing ChatInputModel). - Home / cloud fall back to the agent-profile picker / model picker. - Threads `agent_profile_id` through the conversation-start path (buildStartConversationRequest: agent_profile_id XOR agent_settings; skip the ACP tag / encrypted-settings / subscription check on the profile path) and reads the server's `launched_agent_profile` provenance to mark the current profile without settings-matching. - Replaces the old SwitchProfileButton/context-menu with the new pickers. Validated end-to-end against a real agent-server (SDK main): starting a conversation with `agent_profile_id` returns 201 and stamps `launched_agent_profile { agent_profile_id, revision }`. Ported from #1550's chat implementation. Gates green: typecheck, eslint, prettier, i18n (15 langs), vitest (3496 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(agent-profiles): extract + unit-test buildAgentProfileFields mapping Addresses the code-review feedback that the profile-fields builder — the ACP "built-in default command → null vs verbatim shell string" branch plus the schema-driven tool_concurrency_limit coercion — was the most novel logic in the PR yet had no automated coverage (every test mocked the embedded form away). - Extracts the closure into a pure exported `buildAgentProfileFields()` in agent-settings.tsx; the embedded control now just snapshots state into it. - Adds 8 unit tests locking the round-trip: ACP built-in-default → null, custom command → shell string, custom preset, blank-model → null, OpenHands enable_sub_agents passthrough, concurrency coercion (valid / empty / throws). - Clarifies the service header (client ships in ts-client 1.28.0; the server endpoints it targets shipped in agent-server v1.29.0) per the version-doc nit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): address review feedback + fix e2e regression - Fix mock-LLM E2E regression: the profile-identity spec still targeted the removed `switch-profile-button`; point it at the new `chat-input-llm-profile` picker (mirrors #1550's e2e update). - Use the `useRenameAgentProfile` hook in the editor instead of calling the service directly (the hook was otherwise dead code; now the rename gets list invalidation for free). - Drop the unreachable in-conversation branch from the home AgentProfile picker: the picker only renders on home (a running conversation shows the LLM/model picker), so `useChatInputProfileState` is now home-only (activate as launch default), and the "start new with profile" hint + its CHAT$START_NEW_WITH_PROFILE_HINT key (15 langs) are removed. - Update the two affected tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): correctness fixes from #1571 code review - switch-llm-profile: run the inline "Switched to" message, #1082 metadata persist, and error reporting in mutation-level callbacks so they survive the switcher menu unmounting on select - agent-server-adapter: derive acp_server from agent.acp_server when the acpserver tag is absent, so a profile-launched ACP conversation keeps its model picker and provider chip - use-create-conversation: await the LLM-profile list before the dangling-llm_profile_ref launch guard so a mid-load send can't launch blind - chat-input pickers: read switch/activate pending state via useIsMutating so the pill button actually disables during an in-flight switch - use-activate-agent-profile: surface activation errors (drop disableToast) and optimistically flip active_agent_profile_id with rollback - chat-input-actions: fall back to the LLM picker on the home page when the backend has no /api/agent-profiles surface Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): pass embedded props to reused Agent settings form The profile editor reused AgentSettingsScreen via the route module's default export. React Router's Vite plugin wraps a route default with withComponentProps, which invokes it with route props and drops any props a parent passes — so `embedded`/`onSaveControlChange` never reached it, `saveControl` stayed null, and the Save button was permanently disabled (couldn't create or edit a profile at all). Split the route into a named `AgentSettingsScreen` export (the reusable component embedded consumers import) plus a thin default `AgentSettingsRoute` wrapper, mirroring `LlmSettingsRoute`. The local-view now imports the named export. Updated the unit-test mock to provide the named export (the old mock only stubbed `default`, which is exactly what masked this at unit level). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-profiles): un-gate Agent Profiles on cloud backends The cloud enterprise app-server now exposes the same /api/agent-profiles contract as the local agent-server (OpenHands #15060, epic #3730), so lift the local-only gating and route cloud calls through the cloud proxy. Transport: - cloud/agent-profiles-service.api.ts: CRUD via callCloudProxy (bearer + X-Org-Id) against the identical /api/agent-profiles paths; org resolved server-side from the session, so no {org_id} segment. - cloud/org-profiles-service.api.ts: list org LLM profiles at /api/organizations/{org_id}/profiles so the editor's llm_profile_ref picker works on cloud. Only listing is cloud-routed. - AgentProfilesService + ProfilesService.listProfiles branch to the cloud transport when the active backend is cloud (mirrors SettingsService). Surfaces un-gated: - Settings → Agent profiles nav item + route (no more redirect to /settings/agent). - Home chat-input agent-profile picker (fetch + pickerKind) on cloud. - Launch-from-profile: cloud AppConversationStartRequest now carries agent_profile_id (added to the type + the cloud create request), which the backend resolves and stamps as launched_agent_profile. In-conversation live switch on cloud is intentionally left on the model picker for now: the cloud backend has no per-conversation profile-switch endpoint yet and org LLM-profile detail masks the api_key, so a client-side switch isn't possible — tracked as a follow-up for full parity. Tests updated for the new nav behavior (agent-profiles shown on both). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(agent-profiles): update useAgentProfiles docstring for cloud support Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(agent-profiles): clarify cloud in-conversation switch is intentionally local-only Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): preserve acp_server through the wire normalizer An ACP conversation launched from an agent profile (agent_profile_id) showed a generic chip and an empty in-conversation model picker: the provider identity never reached the UI. Root cause: #1571 taught the conversation adapter to source acp_server from `agent.acp_server` (SDK #3692) when the `acpserver` tag is absent — which is exactly the profile-launch case, since that path doesn't stamp the tag. But `normalizeAgent` (the wire parser feeding the adapter) projected only `{kind, acp_model, llm}` and dropped `acp_server`, so the adapter's fallback always saw undefined → acp_server null → no ACP provider → generic chip + no model list. Add `acp_server` to the normalizeAgent projection (the type already declared it). Regression test covers the no-tag / agent-sourced path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-profiles): make Settings → Agent the profile library Collapse the two Settings sections ("Agent" global form + "Agent profiles" library) into a single "Agent" entry that IS the Agent Profile library: it lists the user's profiles and its create/edit view is the reused Agent settings form plus a name (the embedded AgentSettingsScreen). The active profile is the current agent. - settings-nav: one "Agent" item → /settings/agents (the library). - /settings/agent redirects to /settings/agents; default settings path + ACP route-guard target updated accordingly. Also derive the ACP-enabled state from the ACTIVE AGENT PROFILE rather than settings.agent_settings.agent_kind. Activate is pointer-only and never writes agent_settings, so the global settings are stale when an ACP profile is active; the nav-disable, home ACP context, useLlmConfigured, and the ACP route guard now read the active profile (new useActiveAgentProfile hook) and fall back to settings only while the profile list is loading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): gate the LLM-setup banner on the active agent profile's LLM useLlmConfigured decided "is the LLM ready" from the standalone active LLM profile, but conversations now launch from the active AGENT profile. For an OpenHands profile the relevant LLM is the one it references via llm_profile_ref — not whichever LLM profile happens to be "active". So the "Your LLM isn't set up" banner could be wrong in both directions (e.g. the active LLM profile has a key but the agent profile references a keyless one). Resolve the LLM profile to check from the active agent profile's llm_profile_ref (openhands), falling back to the active LLM profile only when there's no ref yet. ACP agent profiles stay always-configured (subprocess owns its LLM). New unit test covers the discriminating case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-profiles): relabel LLM profile "Active" → "Default" The active LLM profile no longer drives new conversations (the active AGENT profile does) — it's just the default llm_profile_ref seeded into new agent profiles. Relabel the LLM-profile badge "Active" → "Default" and the row action "Set as active" → "Set as default" to stop implying it launches conversations. New i18n keys (SETTINGS$PROFILE_DEFAULT / _SET_DEFAULT, 15 langs). The agent-profile "Active" badge is unchanged — that one IS active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(onboarding): land the user's choice on the active agent profile Onboarding configured global agent_settings + an LLM profile (OpenHands) or ACP secrets, but never touched an AGENT profile — so the active agent profile stayed the seeded `default` (openhands → ref `default`), disconnected from what onboarding set up. Result: an OpenHands user who entered a key still hit "LLM isn't set up" (the active agent profile referenced a keyless profile), and ACP users never got an ACP agent profile at all. Add useApplyOnboardingAgentProfile: upsert + activate the well-known `default` agent profile from the onboarding choice. The OpenHands LLM step now points it at the LLM profile it just created; the ACP secrets step makes it an ACP profile for the chosen provider (opus[1m]/valid default, no LLM key needed). Verified e2e: OpenHands onboarding → default agent profile refs the configured LLM + banner clears; Claude Code onboarding → default agent profile is acp/claude-code, active, no LLM required. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: drop unused eslint-disable in onboarding agent-profile hook Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(agent-profiles): gate mutate controls for cloud view-only members Reuse #1532's org-permission gating for the Agent Profiles UI. Agent profiles are org-scoped on cloud (bearer + X-Org-Id, edit_org_settings), so a cloud member previously saw Add/Edit/Delete/Set-active controls that would 403 server-side — the same flash-then-403 problem #1532 fixed for LLM profiles. - Generalize useCanManageLlmProfiles -> useCanManageOrgProfiles (it reads the generic edit_org_settings permission; local users always true). - Thread canManage through AgentProfilesManager -> Body -> Row, mirroring LlmProfilesManager: hide the Add button and the row actions menu for view-only members. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: fix stale comments surfaced by PR review Comment-only. No behavior change. - chat-input-actions.tsx: the pickerKind summary claimed "cloud → model picker (cloud has no profile surface)", contradicting the code, which uses the AgentProfile picker on cloud home too (#15060). Rewrite to match the actual cases; trim the duplicated render-site recap. - acp-route-guard.ts / settings-nav.tsx / settings.tsx: the ACP redirect target moved to /settings/agents (plural) in this PR, but three docstrings still said /settings/agent. Update them. * test(mock-llm-e2e): wire the active agent profile to the mock LLM Fixes the mock-LLM e2e regression where the home composer stayed blocked (submit disabled / launcher never ready) so conversation-launching specs timed out. Conversations now launch from the active AGENT profile (#1571), and `useLlmConfigured` follows that profile's `llm_profile_ref` — not the active LLM profile. The specs seed `openhands-onboarded` and configure an LLM profile the old way, so the seeded "default" agent profile still pointed at a keyless LLM and the composer never unblocked. Mirror what onboarding does for a real user: after activating the mock LLM profile, upsert + activate the "default" agent profile referencing it. Add a shared `ensureMockLLMAgentProfile` helper (called from ensureMockLLMProfile and from the conversation spec, which sets up inline). Verified locally: the full mock-llm-conversation spec passes 4/4 (real conversation runs against the mock LLM) with this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): address PR #1571 review feedback Human review (VascoSch92): - useLlmConfigured: fall back to the active LLM profile when the active agent profile's llm_profile_ref is stale/absent, mirroring the launch-time fallback in useCreateConversation. Without this the two contradicted each other: launch succeeded via the fallback but the hook reported unconfigured and spuriously disabled the composer + banner (even inside a running conversation). Adds a regression test for the stale-ref scenario. - Drop the dead launched_profile plumbing (wire parse + types + adapter map): it had zero readers (the home picker keys off active_agent_profile_id and the in-conversation picker is LLM/model by design), so the "Consumed by the picker" comments were misleading. - Point the remaining /settings/agent links at /settings/agents (ACP model context, chat-input model state, chat error re-auth, command menu) so the route rename doesn't cost an extra redirect hop. /codereview-roasted: - Extract the triple-nested pickerKind ternary into a pure, unit-tested resolvePickerKind() helper. - Document why cloud OpenHands onboarding intentionally does not repoint the active agent profile (persistAsProfile is local-only; cloud resolves the agent-profile/LLM wiring server-side). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(agent-profiles): scope the profile-launch enrichment gap (#1571 review) - Document at buildStartConversationRequest that the profile path relies on the server/SDK to restore exec tools + public skills (software-agent-sdk#3967), and that canvas_ui + the RUNTIME_SERVICES suffix are intentionally canvas-only. - Point the createConversation positional-args TODO at the tracked issue (#1587). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): preserve unmodeled fields on edit-save + await profiles at launch Two fixes from the #1571 review: Edit-save wiped every profile field the minimal editor doesn't model (condenser, verification, system_message_suffix, skill/MCP refs, embedded skills, ACP session mode/timeout): the save endpoint is a whole-profile overwrite, and the editor posted only its own fields. The save payload now spreads the stored profile under the edited fields via a pure, kind-aware mergeAgentProfileSaveInput — a kind switch stays a clean variant replacement (the server's extra="forbid" union rejects mongrel payloads), and server-managed identity (id/name/revision) is stripped. The edit fetch now uses X-Expose-Secrets: encrypted so any skills[].mcp_tools values round-trip as Fernet tokens instead of persisting the mask literally (same pattern as the LLM-profile editor). Launch raced the agent-profiles query: useCreateConversation read the hook's maybe-unresolved data, so a send fired before the list loaded fell through to the stale global agent_settings path — which activation (pointer-only) never updates — and silently launched the wrong agent. The launch now awaits the list via queryClient.ensureQueryData on the shared query key (mirroring the LLM-profile ref validation below it), with retry: false so backends without the surface degrade to the legacy launch immediately. The dangling-llm-ref downgrade also logs a console.warn so the silent fallback is diagnosable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(acp): drive ACP spec through the Agent Profile editor, not the retired route Settings → Agent is now the Agent Profile library (#1571): the standalone /settings/agent form redirects to /settings/agents, whose editor reuses the same embedded agent-settings-screen form. The ACP mock-llm spec and the resetToOpenHandsAgentViaUI cleanup helper still navigated the old route and waited on the retired agent-save-button, so they timed out — and the cleanup helper's failure (swallowed by afterAll's try/catch) left the "default" agent profile stuck in ACP mode, poisoning downstream specs that share the backend. - Add openAgentProfileEditor(page, name): navigate /settings/agents, open the named profile's editor via its row action menu (row located by the profile-name span[title], mirroring activateProfileViaUI). - Rewrite resetToOpenHandsAgentViaUI to drive the new editor (switch kind → OpenHands, pick an LLM profile, save via save-agent-profile-btn). - Point ACP spec steps 1 & 2 at the editor; swap agent-save-button → save-agent-profile-btn. - Verify step 1 against GET /api/agent-profiles/default (the new source of truth) instead of legacy /api/settings; acp_command is a shell string there (ts-client AgentProfile.acp_command: string | null), not a token array. * fix(build): keep the styling core in one chunk to avoid a tv() init-order crash This PR's new imports grew/shifted the auto-split `vendor` chunk enough that Rolldown's size-based splitter (`maxSize`) sliced the styling core apart — separating a HeroUI component's top-level `tv()` recipe from tailwind-variants' core within the emitted init order. The recipe then evaluated before tailwind-variants initialized, throwing `TypeError: s is not a function` at module load. React Router reported "Error loading route module root-layout, reloading page", looped, and rendered a blank page — deterministically crashing the whole app and failing 13 mock-llm-e2e specs (npm + docker) that load the shell. Give the styling core (@heroui/react + tailwind-variants + tailwind-merge + clsx) its own group that is never size-split, so it initializes as a coherent unit before any consumer's top-level `tv()` call. Verified locally: the home route renders (was a blank page) with zero console errors. * test(mock-llm): make LLM-profile setup idempotent and fix stale ACP launch assertion With the crash fixed, the app renders and a second class of failure surfaced: specs that call `ensureMockLLMProfile` after the first one deadlocked on a stuck "Delete Profile" modal, and the ACP spec's payload assertion checked the old launch shape. - ensureMockLLMProfile: create the mock LLM profile only when absent instead of delete-then-recreate. Once the active agent profile references it (wired right after, via ensureMockLLMAgentProfile — #1571), the LLMProfile FK guard rejects deletion; the delete-confirm modal then silently stays open and its backdrop blocks every later click (`add-llm-profile` timed out across files, home, automations, mcp, model-switch, preset-automation). The mock config is deterministic, so reusing an existing same-named profile is correct. deleteProfileIfExists is unchanged — it still works for the non-referenced profiles that other specs delete. - mock-llm-acp-agent step 3: conversations now launch from the active AgentProfile (#1571), so the POST /api/conversations payload carries `agent_profile_id` and omits `agent_settings` (mutually exclusive, per agent-server-adapter). Assert that shape instead of the retired `agent_settings.agent_kind`; the ACP reply-token check still proves the ACP agent ran. * ci: degrade gracefully when the linked SDK reference isn't a PR "Resolve linked SDK PR" (mock-llm-docker-e2e.yml) greps the PR description for OpenHands/software-agent-sdk#NNNN or .../pull/NNNN and tries to build against that PR's branch. GitHub's "#NNNN" shorthand looks identical for issues and PRs, so a description that links a tracking issue (e.g. #3713) matches the same regex — and /pulls/{number} 404s for an issue number, failing the whole job under `bash -e` instead of falling back to the released SDK version like the "no match" branch already does. Treat a failed PR lookup the same as "no linked PR found": log and exit 0, leaving git_ref unset so the job falls through to the released version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): make ensureMockLLMProfile/AgentProfile converge, not skip Two real e2e failures traced to test-helper bugs surfaced only once #3968 (SDK 1.32.0) let profile-launched conversations actually run: - ensureMockLLMProfile: the earlier idempotent-reuse fix (deadlock guard against the LLMProfile FK constraint) skipped writing the profile's config entirely whenever a same-named profile already existed — correct for repeat calls with the SAME config, but silently ignored a DIFFERENT one. mock-llm-image-upload requests a vision-capable model ("openai/gpt-4o") to get past the mock LLM's default; when an earlier spec in the same CI run had already created "mock-llm" with the default model, the override never applied and the agent replied "the currently selected model does not support image understanding" — confirmed via the CI screenshot. Fixed by editing the existing profile in place (via the LLM settings UI's Edit flow, never deleting it) so every call converges on the requested model/apiKey/baseUrl regardless of what an earlier test left behind. - ensureMockLLMAgentProfile: OpenHandsAgentProfile.skill_refs defaults to `[]` (none discovered) when omitted from the save payload. Workspace- scoped project skills are discovered independently of this and keep working, but a profile-launched conversation's agent never sees any public/preset skill (e.g. an installed automation's bundled skill) without an explicit skill_refs. Set it to `null` (all discovered), matching what a real onboarding-seeded profile effectively gets. mock-llm-model-switch step 2's post-switch reply timeout is left unaddressed: its trajectory hard-codes one padding turn for "the agent-server's internal condenser/skill-analysis call before the main loop" (a documented, historically-fragile assumption per the test's own comment) — plausibly now off by one now that #3968 lets the agent make additional real tool-use calls around a /model switch. Fixing this requires an empirical trajectory-turn count from a real 1.32.0 conversation trace, which needs a CI cycle to observe correctly rather than guessing blind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert(test): drop skill_refs=null from ensureMockLLMAgentProfile CI showed this regressed mock-llm-skills.spec.ts (project skill in workspace/.agents/skills/), which passed before this change: fixed the narrow preset-automation slash-command skill-activation case at the cost of breaking a more fundamental, previously-solid #3968 validation — a net-negative trade, not a clean win. The shared "default" agent profile backs every spec in the suite; widening its skill_refs to "all discovered" has global blast radius across unrelated tests, evidently including some interaction with project-skill discovery/activation tracking that isn't understood yet. A fix for preset-automation's specific skill needs to be scoped to that one profile/test, not applied to the profile every other spec shares. Keeps the ensureMockLLMProfile edit-in-place fix (proven, isolated, fixes mock-llm-image-upload with no observed side effects). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): default new profiles' skill_refs to "all discovered" OpenHandsAgentProfile.skill_refs defaults to `[]` (none) server-side when omitted from a save payload. Neither onboarding's profile seed nor the Settings "Add Agent Profile" editor exposes a skill_refs control, so every newly-created profile silently gets zero public/user/project skills — a profile-launched conversation's agent can't activate any of them (#1571 launches conversations from the active agent profile). This is exactly the mock-llm-preset-automation regression: a slash-command-triggered skill never activates because the "default" test profile has no skill_refs, matching what a real user's fresh profile would also hit. useSaveAgentProfile is the single choke point for every profile save (onboarding seed + Settings create/edit), so default skill_refs to `null` ("all discovered") there whenever the caller hasn't set it explicitly — matches what users actually expect (a new agent has access to their skills unless deliberately scoped down) and requires no SDK change. The pinned typescript-client doesn't type skill_refs on AgentProfileSaveInput yet (SDK/wire drift), so this reaches it via an untyped merge; `in` checks the runtime object since mergeAgentProfileSaveInput's edit-preserve spread can carry it at runtime despite the missing type. Re-applies the equivalent default to ensureMockLLMAgentProfile (the e2e test helper bypasses this hook via a raw fetch) so the test suite mirrors real behavior. Verified: full unit suite green (3618 passed), typecheck clean, agent-profiles-local-view.test.tsx passes unaffected (it mocks useSaveAgentProfile at the hook boundary, so this change is invisible to it). Locally reproduced the fix: mock-llm-preset-automation's slash- command skill-activation test now passes; mock-llm-image-upload (previously fixed) still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): self-heal LLM profile stream=true for profile-launched conversations A profile-launched conversation (agent_profile_id) never sends agent_settings, so PR #1474's `llm.stream = true` (buildConfiguredOpenHands AgentSettings, agent-server-adapter.ts) never reaches it — the referenced LLM profile's stored `stream` field (SDK default: false) is used as-is by resolve_agent_profile/_build_openhands_settings, with no override, unlike the legacy path. The agent-server decides once, at conversation construction, whether to wire the `on_token` streaming callback — based on whether any of the agent's LLMs has stream=True at that moment — and never re-evaluates it afterward (confirmed by reading LocalConversation.switch_llm: it swaps the LLM but never touches _on_token). So a profile-launched conversation whose LLM profile was never saved with stream=true gets on_token=None for its entire lifetime. switchProfile's switch_llm call (unconditionally sending stream: true, unchanged by this fix) then crashes the next completion with "Streaming requires an on_token callback", since on_token can never be (re-)wired post-construction. Confirmed via real agent-server tracebacks in both mock-llm-e2e and mock-llm-docker-e2e CI runs. Streaming is a pre-existing, independently-shipped feature (PR #1474) that must not regress for legacy-launched conversations — ruling out simply dropping switch_llm's stream:true (would silently disable streaming after a switch for the one case that works today). And since existing users' LLM profiles predate this fix, defaulting stream:true only at future profile-save time (mirroring the skill_refs fix) would still crash on their first profile-launched conversation post-deploy. ensureLlmProfileStreams is a migration shim: at the one call site guaranteed to run for every profile-launched conversation (already fetching the LLM-profiles list to validate llm_profile_ref exists), check the referenced LLM profile's full config and, if stream isn't already true, save it with stream:true — self-healing both new and existing profiles on first use, memoized per profile name for the session so it's a no-op read on every subsequent launch. Mirrors the profile-duplicate flow's exact pattern for round-tripping the encrypted secret (getProfile(name, "encrypted") + saveProfile(..., include_secrets: true)) so the stored api_key is never clobbered. Touches neither the legacy agent_settings path nor switch_llm — both keep working exactly as before. Safe to delete once virtually all users are migrated, or once resolve_agent_profile forces stream=true for OpenHands profiles upstream (same category of fix as the skill_refs default — likely the same #3967 umbrella), whichever comes first. Verified: full unit suite green (3620 passed, +2 new tests exercising this exact self-heal/no-op branching), typecheck clean. Locally reproduced the fix: mock-llm-model-switch's on_token crash no longer occurs; preset-automation and image-upload remain passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(agent-profiles): link the skill_refs/streaming shims to their tracking issue References OpenHands/agent-canvas#1619 (the cleanup-tracking issue for both workarounds) and the specific upstream SDK issues, so the removal criteria is discoverable from the code itself, not just the PR description. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): drop skill_refs/streaming migration shims (SDK #4017 landed) software-agent-sdk#4017 (PR #4018) fixes both gaps these shims worked around: OpenHandsAgentProfile.skill_refs now defaults to null (all discovered) server-side, and the agent-server forces llm.stream=true for profile-launched conversations. Both shims are now dead code. Validated end-to-end against the SDK branch (OH_AGENT_SERVER_LOCAL_PATH) before removing: real HTTP round-trips confirmed skill_refs defaults to null and the launched agent's LLM streams even though the underlying LLM profile is stored with stream=false; the full mock-llm-skills.spec.ts and mock-llm-profile-management.spec.ts suites pass unchanged. Removes: - withDefaultSkillRefs (src/hooks/mutation/use-save-agent-profile.ts) - ensureLlmProfileStreams + its two dedicated tests (src/hooks/mutation/use-create-conversation.ts) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs(agent-profiles): correct comments for the disabled_skills deny-list SDK #4017 replaced the profile's skill_refs allow-list (and embedded skills) with a disabled_skills deny-list. Canvas is already deny-list-native — the user-level disabled_skills UI exists and the generic profile merge carries the field automatically — so only two stale comments referencing embedded skills / skill refs needed correcting. No functional change; the per-profile skill picker stays out of scope for the minimal editor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(agent-profiles): drop stale skill_refs from fixtures for the deny-list SDK #4017 replaced the profile's skill_refs allow-list (and embedded skills) with a disabled_skills deny-list. Update the fixtures/comments that still referenced the removed fields (they ride untyped through `as unknown` casts / raw POST bodies, so the generic merge round-trips them regardless): - merge-agent-profile-save-input.test.ts + agent-profiles-local-view.test.tsx: skill_refs -> disabled_skills, drop embedded `skills`, schema_version 3, ACP fixtures drop the skill field (ACP has none). Correct the stale exposeSecrets/mcp_tools comment (profiles are secret-free now). - mock-llm-helpers.ts: the omitted-field comment now describes the deny-list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(agent-profiles): profile fixtures use the v1 baseline schema_version SDK #4017 collapsed the pre-ship AgentProfile schema history to a clean v1 baseline (no v2/v3, no migrations). Update the two profile fixtures to schema_version: 1 to match the shipped model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): launch the `default` profile via agent_settings; stamp the launched LLM ref The seeded `default` agent profile is the enriched baseline that mirrors global agent_settings, not a deliberate profile pick. Launching it via `agent_profile_id` made the server rebuild the agent purely from the profile, dropping the canvas-only enrichments the profile-resolution path can't carry — the `<RUNTIME_SERVICES>` system-message suffix, the `canvas_ui` tool, and project-skill loading. Route the well-known `default` profile through the agent_settings launch instead; named profiles are deliberate custom configs and keep the profile path. Fixes the mock-llm-docker-e2e automation RUNTIME_SERVICES failure. Also from #1571 review: - Stamp the launched OpenHands profile's `llm_profile_ref` into conversation metadata (not the standalone active LLM profile) so the switcher pill names the exact profile the conversation runs when the two differ (#1082). - Add `retry: false` to the LLM-ref validation fetch, matching the sibling agent-profiles fetch, so a slow/erroring /api/profiles falls back promptly. Hoist the well-known name to `WELL_KNOWN_DEFAULT_AGENT_PROFILE_NAME` (shared by the launch path and onboarding). Re-onboarding intentionally overwrites `default`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): gate the LLM-setup banner on the active agent-profile load `useLlmConfigured` derives `isAcpAgent` and the referenced LLM from the active agent profile but omitted that query's loading state from `isLoading`. On a cold cache an ACP agent (which needs no key) briefly read as an unconfigured OpenHands agent, flashing the "LLM not set up" banner until the profiles query resolved. Thread the `useActiveAgentProfile` loading signal into the indeterminate state so consumers render nothing until the active agent profile is known (#1571 review). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): scope the default→agent_settings launch to OpenHands profiles The `default`→agent_settings shortcut (which preserves <RUNTIME_SERVICES>/canvas_ui) must not apply to an ACP `default` profile: activation is pointer-only, so global agent_settings is stale (still OpenHands) when an ACP profile is active — routing it via agent_settings launched the wrong agent (mock-llm-acp-agent.spec.ts step 3 expected agent_profile_id, got OpenHands agent_settings). ACP also carries no <RUNTIME_SERVICES>/canvas_ui enrichment, so there's nothing to preserve. Gate the shortcut on agent_kind === "openhands"; ACP defaults keep the profile path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(agent-profiles): address PR #1571 review findings (VascoSch92) - Gate the default-profile agent_settings downgrade to local backends only; cloud always launches from the resolved agent_profile_id. - Emit an explicit schema-default (not an omitted key) when tool_concurrency_limit is cleared, so edit-save actually resets it. - Restore the tailored "Switched to {name} failed" toast via meta.disableToast + a dedicated onError. - Share one AGENT_PROFILES_RETRY_OPTIONS constant across the launch path, redirectIfAcpActive, and useAgentProfiles so retry policy can't drift between call sites. - Fix a stale comment on optimisticActiveProfile's write path. - Self-heal a dangling llm_profile_ref in the agent-profile editor by validating it against the live LLM-profiles list on load. * fix(agent-profiles): restore cloud in-conversation LLM-profile switching resolvePickerKind hard-coded cloud conversations to the read-only model picker, on the premise that cloud has no per-conversation switch endpoint. That's not true: POST /api/v1/app-conversations/{id}/switch_profile has existed since OpenHands#14288 (2026-05-05), predating this PR, and the frontend plumbing to call it (AgentServerConversationService.switchProfile's cloud branch) was already implemented and just unreachable. Cloud OpenHands conversations now resolve to the LLM-profile picker, same as local, matching how ACP already behaves identically on both backends. main's old SwitchProfileButton had no cloud gate either, so this restores previously-working behavior rather than adding new scope. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>