Download src/hooks/mutation/use-create-conversation.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/hooks/mutation/use-create-conversation.ts
- Command line
-
hf download hf://SaylorTwift/openhands/src/hooks/mutation/use-create-conversation.ts
-
curl -L -o use-create-conversation.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/src/hooks/mutation/use-create-conversation.ts
17.5 kB
| import { useMutation, useQueryClient } from "@tanstack/react-query"; | |
| import AgentServerConversationService from "#/api/conversation-service/agent-server-conversation-service.api"; | |
| import { PluginSpec } from "#/api/conversation-service/agent-server-conversation-service.types"; | |
| import { SuggestedTask } from "#/utils/types"; | |
| import { Provider } from "#/types/settings"; | |
| import { useTracking } from "#/hooks/use-tracking"; | |
| import { useLlmProfiles } from "#/hooks/query/use-llm-profiles"; | |
| import { useAgentProfiles } from "#/hooks/query/use-agent-profiles"; | |
| import { useActiveBackend } from "#/contexts/active-backend-context"; | |
| import ProfilesService from "#/api/profiles-service/profiles-service.api"; | |
| import AgentProfilesService, { | |
| WELL_KNOWN_DEFAULT_AGENT_PROFILE_NAME, | |
| type AgentProfileListResponse, | |
| } from "#/api/agent-profiles-service/agent-profiles-service.api"; | |
| import PluginsManagementService, { | |
| type InstalledPluginInfo, | |
| } from "#/api/plugins-management-service"; | |
| import { | |
| PLUGINS_QUERY_KEYS, | |
| LLM_PROFILES_QUERY_KEYS, | |
| AGENT_PROFILES_QUERY_KEYS, | |
| AGENT_PROFILES_RETRY_OPTIONS, | |
| } from "#/hooks/query/query-keys"; | |
| import { pluginReferenceKey } from "#/utils/plugin-display"; | |
| import { | |
| getStoredConversationMetadata, | |
| setStoredConversationMetadata, | |
| toPluginCoordinates, | |
| type WorkspaceMode, | |
| } from "#/api/conversation-metadata-store"; | |
| export interface CreateConversationVariables { | |
| query?: string; | |
| repository?: { | |
| name: string; | |
| gitProvider: Provider; | |
| branch?: string; | |
| }; | |
| suggestedTask?: SuggestedTask; | |
| conversationInstructions?: string; | |
| parentConversationId?: string; | |
| agentType?: "default" | "plan"; | |
| plugins?: PluginSpec[]; | |
| workingDir?: string; | |
| workspaceMode?: WorkspaceMode; | |
| // Launch from a specific AgentProfile (local backend). When omitted, the | |
| // active AgentProfile (if any) is used so home-composed conversations | |
| // launch from the user's selected profile (#3727). | |
| agentProfileId?: string; | |
| entryPoint?: string; // analytics only; not forwarded to the service | |
| } | |
| export const CREATE_CONVERSATION_MUTATION_KEY = ["create-conversation"]; | |
| interface CreateConversationResponse { | |
| conversation_id: string; | |
| session_api_key: string | null; | |
| url: string | null; | |
| task_id?: string; | |
| } | |
| export const useCreateConversation = () => { | |
| const queryClient = useQueryClient(); | |
| const { trackConversationCreated } = useTracking(); | |
| // Cache-warm on the home page (the profile picker reads the same query). | |
| // Stamped onto the conversation at creation so the switcher can show the | |
| // exact profile even when several profiles share a model (#1082). | |
| const { data: llmProfiles } = useLlmProfiles(); | |
| // Warm the agent-profiles cache too — the launch path below awaits the same | |
| // query via ensureQueryData, so a warm cache makes home-launch instant. The | |
| // hook's maybe-unresolved data is deliberately NOT read at launch time: | |
| // activation is pointer-only (it never writes agent_settings), so racing a | |
| // cold cache into the agent_settings fallback would silently launch the | |
| // wrong agent. | |
| const { backend, orgId } = useActiveBackend(); | |
| useAgentProfiles(); | |
| return useMutation({ | |
| mutationKey: CREATE_CONVERSATION_MUTATION_KEY, | |
| mutationFn: async ( | |
| variables: CreateConversationVariables, | |
| ): Promise<CreateConversationResponse> => { | |
| const { | |
| query, | |
| conversationInstructions, | |
| plugins, | |
| repository, | |
| workingDir, | |
| workspaceMode, | |
| parentConversationId, | |
| agentType, | |
| agentProfileId, | |
| } = variables; | |
| // The active AgentProfile is the default launch profile for new | |
| // conversations (#3727), on both local and cloud (cloud gained | |
| // /api/agent-profiles in OpenHands #15060, #3730). Await the list from | |
| // the shared query cache: a send fired before the home query resolves | |
| // must still launch from the active profile. Do not fall back to the | |
| // global agent_settings when profile discovery fails: activation is | |
| // pointer-only, so those settings may describe a different agent. | |
| const agentProfiles: AgentProfileListResponse = | |
| await queryClient.ensureQueryData({ | |
| queryKey: [...AGENT_PROFILES_QUERY_KEYS.all, backend.id, orgId], | |
| queryFn: AgentProfilesService.listProfiles, | |
| ...AGENT_PROFILES_RETRY_OPTIONS, | |
| }); | |
| const requestedAgentProfileId = | |
| agentProfileId ?? agentProfiles.active_agent_profile_id ?? undefined; | |
| // Fall back to the legacy agent_settings launch when the resolved agent | |
| // profile can't resolve its LLM. The agent-server seeds a `default` | |
| // openhands profile whose `llm_profile_ref` can point at an LLM profile | |
| // that doesn't exist (fresh store, or one configured with named profiles | |
| // only); launching from it 404s ("LLM profile '<ref>' not found") and | |
| // would brick home-launch. agent_settings reflects the active LLM, so the | |
| // fallback degrades cleanly until the seed mirrors it (SDK #3933). | |
| // ACP profiles carry no llm_profile_ref, so they're never gated here. | |
| const resolvedAgentProfile = requestedAgentProfileId | |
| ? agentProfiles?.profiles?.find( | |
| (profile) => profile.id === requestedAgentProfileId, | |
| ) | |
| : undefined; | |
| // Cloud has no `agent_settings` payload to fall back to — the downgrade | |
| // below only makes sense on local, where it exists and carries the | |
| // canvas-only enrichments. Gating it here keeps cloud always launching | |
| // from the resolved profile id, so the conversation gets | |
| // `launched_agent_profile` stamped and the profile's config applied | |
| // (#1571 review). | |
| const isCloud = backend.kind === "cloud"; | |
| let effectiveAgentProfileId = requestedAgentProfileId; | |
| // The account-wide active LLM profile from the launch-path fetch below | |
| // (null when that fetch didn't run or failed). Fresher than the | |
| // `useLlmProfiles()` render snapshot, which a fast send can outrun. | |
| let fetchedActiveLlmProfile: string | null = null; | |
| if ( | |
| !isCloud && | |
| resolvedAgentProfile?.name === WELL_KNOWN_DEFAULT_AGENT_PROFILE_NAME && | |
| resolvedAgentProfile?.agent_kind === "openhands" | |
| ) { | |
| // The seeded OpenHands `default` profile is the enriched baseline, not a | |
| // deliberate profile pick — it mirrors global agent_settings. Launch it | |
| // via agent_settings so the canvas-only enrichments the profile-resolution | |
| // path drops survive for the common home-launch: the <RUNTIME_SERVICES> | |
| // system-message suffix and project-skill loading (buildAgentContext). | |
| // Named profiles are deliberate custom configs and still use the profile | |
| // path (accepting that enrichment boundary). | |
| // Trade-off: per-profile fields set on `default` itself don't apply on | |
| // home-launch — custom per-profile config belongs in a named profile. | |
| // | |
| // Scoped to OpenHands: an ACP `default` must keep the profile path. | |
| // Activation is pointer-only, so global agent_settings is stale (often | |
| // still OpenHands) when an ACP profile is active — launching it via | |
| // agent_settings would start the wrong agent. ACP carries no | |
| // <RUNTIME_SERVICES> enrichment, so there's nothing to preserve. | |
| // | |
| // Scoped to local: cloud never writes agent_settings, so it always | |
| // resolves `default` server-side via agent_profile_id (validated below). | |
| effectiveAgentProfileId = undefined; | |
| } else if ( | |
| resolvedAgentProfile?.agent_kind === "openhands" && | |
| resolvedAgentProfile.llm_profile_ref | |
| ) { | |
| // Await the LLM-profile list rather than reading the maybe-unresolved | |
| // `useLlmProfiles()` result: a send fired before that query loads (or | |
| // after it errors) must still validate the ref, not launch blind. | |
| let llmProfileExists = false; | |
| try { | |
| const llm = await queryClient.ensureQueryData({ | |
| queryKey: [...LLM_PROFILES_QUERY_KEYS.all, backend.id, orgId], | |
| queryFn: ProfilesService.listProfiles, | |
| // Match the agent-profiles fetch above: on a backend where this | |
| // errors, fall back to agent_settings immediately rather than | |
| // stalling the send through the default exponential backoff. | |
| retry: false, | |
| }); | |
| llmProfileExists = llm.profiles.some( | |
| (profile) => profile.name === resolvedAgentProfile.llm_profile_ref, | |
| ); | |
| fetchedActiveLlmProfile = llm.active_profile ?? null; | |
| } catch { | |
| // List unavailable → can't validate → fall back to agent_settings. | |
| } | |
| if (!llmProfileExists) { | |
| // Downgrade is silent in the UI; leave a diagnosable trace. | |
| console.warn( | |
| `Agent profile "${resolvedAgentProfile.name}" references missing ` + | |
| `LLM profile "${resolvedAgentProfile.llm_profile_ref}"; ` + | |
| "launching from agent_settings instead.", | |
| ); | |
| effectiveAgentProfileId = undefined; | |
| } else if ( | |
| !isCloud && | |
| !agentProfileId && | |
| fetchedActiveLlmProfile && | |
| fetchedActiveLlmProfile !== resolvedAgentProfile.llm_profile_ref | |
| ) { | |
| // The home LLM pill shows — and its dropdown activates — the | |
| // account-wide active LLM profile, never the pinned ref | |
| // (useChatInputLlmProfileState), so when the two differ the launch | |
| // must run the selection or the UI advertises a model the | |
| // conversation won't use (#16539). Launch via agent_settings, which | |
| // the dropdown's profile activation syncs to the selection. Scoped | |
| // to the implicit active-profile launch: an explicit `agentProfileId` | |
| // (the in-conversation profile picker) is a deliberate profile pick, | |
| // so its pinned ref stays authoritative. Local-only like the | |
| // downgrades above — cloud has no agent_settings payload to fall | |
| // back to. Trade-off: the named profile's non-LLM config doesn't | |
| // apply to this launch; the start request has no per-launch LLM | |
| // override that could preserve it (AgentLaunchAdditions carries only | |
| // a system-message suffix). | |
| effectiveAgentProfileId = undefined; | |
| } | |
| } | |
| // Only extend the call with the profile fields when launching from a | |
| // profile, so a plain create stays byte-identical to the legacy | |
| // agent_settings path (#3727). sandboxId is unused here. | |
| const conversation = | |
| await AgentServerConversationService.createConversation({ | |
| initialUserMsg: query, | |
| conversationInstructions, | |
| plugins, | |
| metadata: repository | |
| ? { | |
| selected_repository: repository.name, | |
| selected_branch: repository.branch ?? null, | |
| git_provider: repository.gitProvider, | |
| } | |
| : null, | |
| workingDirOverride: workingDir, | |
| workspaceMode, | |
| parentConversationId, | |
| agentType, | |
| ...(effectiveAgentProfileId | |
| ? { | |
| agentProfileId: effectiveAgentProfileId, | |
| agentProfileKind: resolvedAgentProfile?.agent_kind, | |
| } | |
| : {}), | |
| }); | |
| // Stamp the active LLM profile onto the (local) conversation so the | |
| // chat switcher shows the exact profile even when several profiles | |
| // share a model (#1082). Cloud conversations don't use local profiles | |
| // (app_conversation_id stays null until the sandbox is READY). Merge so | |
| // the repo/workspace metadata the service just persisted is preserved. | |
| const localConversationId = conversation.app_conversation_id; | |
| // Snapshot the conversation's plugins into client-side metadata so the | |
| // in-conversation plugins view can show what's loaded (coordinates only | |
| // — strip parameters, which may carry secrets). The agent-server doesn't | |
| // return a live conversation's loaded plugins, so this snapshot is the | |
| // source for that view. Two sources, deduped by coordinates: | |
| // 1. plugins explicitly attached at creation (e.g. the /launch flow); | |
| // 2. enabled installed plugins, which the SDK auto-loads into every new | |
| // local conversation (see use-set-plugin-enabled). | |
| const explicitPlugins = plugins?.map(toPluginCoordinates) ?? []; | |
| let attachedPlugins: PluginSpec[] = explicitPlugins; | |
| if (localConversationId) { | |
| let installed: InstalledPluginInfo[] = []; | |
| try { | |
| installed = await queryClient.ensureQueryData({ | |
| // Same key `usePlugins` writes. Unscoped, this served the previous | |
| // backend's list and attached plugins the target server lacks. | |
| queryKey: [...PLUGINS_QUERY_KEYS.installed, backend.id, orgId], | |
| queryFn: () => PluginsManagementService.listInstalledPlugins(), | |
| }); | |
| } catch { | |
| // Best-effort: never let plugin lookup block conversation creation. | |
| } | |
| const seen = new Set(explicitPlugins.map(pluginReferenceKey)); | |
| const enabledInstalled = installed | |
| .filter((plugin) => plugin.enabled) | |
| .map((plugin) => ({ | |
| source: plugin.source, | |
| ref: plugin.resolved_ref ?? null, | |
| repo_path: plugin.repo_path ?? null, | |
| // Keep the human-friendly name so the plugins view shows e.g. | |
| // "city-weather" rather than deriving "local" from the source. | |
| name: plugin.name, | |
| })) | |
| .filter((plugin) => !seen.has(pluginReferenceKey(plugin))); | |
| attachedPlugins = [...explicitPlugins, ...enabledInstalled]; | |
| } | |
| // A launch from a named OpenHands profile runs that profile's | |
| // `llm_profile_ref`, which can differ from the standalone active LLM | |
| // profile — stamp the ref so the switcher pill names the exact profile | |
| // the conversation runs (#1082). The agent_settings paths (the `default` | |
| // baseline, a dangling ref, or a dropdown override (#16539) — where | |
| // effectiveAgentProfileId is cleared) run the active LLM, so they stamp | |
| // the active profile, preferring the launch-path fetch over the hook's | |
| // render snapshot. ACP profiles carry no LLM profile, so they fall | |
| // through to the active-profile stamp (unused by the ACP model chip). | |
| const activeProfile = | |
| effectiveAgentProfileId && | |
| resolvedAgentProfile?.agent_kind === "openhands" | |
| ? resolvedAgentProfile.llm_profile_ref | |
| : (fetchedActiveLlmProfile ?? llmProfiles?.active_profile ?? null); | |
| if (localConversationId && (activeProfile || attachedPlugins.length)) { | |
| const prev = getStoredConversationMetadata(localConversationId); | |
| setStoredConversationMetadata(localConversationId, { | |
| selected_repository: prev?.selected_repository ?? null, | |
| selected_branch: prev?.selected_branch ?? null, | |
| git_provider: prev?.git_provider ?? null, | |
| selected_workspace: prev?.selected_workspace ?? null, | |
| workspace_mode: prev?.workspace_mode ?? null, | |
| active_profile: activeProfile ?? prev?.active_profile ?? null, | |
| plugins: attachedPlugins.length | |
| ? attachedPlugins | |
| : (prev?.plugins ?? null), | |
| }); | |
| } | |
| // OpenHands cloud pattern: when the start task isn't immediately | |
| // READY (cloud sandbox is still provisioning), | |
| // app_conversation_id is null. We return a `task-{id}` URL so the | |
| // conversation route's useTaskPolling can drive it to READY and | |
| // then redirect to the real `/conversations/{app_conversation_id}`. | |
| const conversationId = conversation.app_conversation_id | |
| ? conversation.app_conversation_id | |
| : `task-${conversation.id}`; | |
| return { | |
| conversation_id: conversationId, | |
| session_api_key: null, | |
| url: conversation.agent_server_url, | |
| task_id: conversation.id, | |
| }; | |
| }, | |
| onSuccess: async (data, variables) => { | |
| trackConversationCreated({ | |
| conversationId: data.conversation_id, | |
| taskId: data.task_id, | |
| hasRepository: !!variables.repository, | |
| gitProvider: variables.repository?.gitProvider, | |
| hasWorkspace: !!variables.workingDir, | |
| workspaceMode: variables.workspaceMode, | |
| hasInitialQuery: !!variables.query, | |
| agentType: variables.agentType, | |
| hasParentConversation: !!variables.parentConversationId, | |
| entryPoint: variables.entryPoint, | |
| }); | |
| // Invalidate (rather than remove) so the existing paginated list stays | |
| // rendered while a background refetch picks up the new conversation. | |
| // `removeQueries` would wipe the cache and force the panel back to its | |
| // initial loading state, dropping loaded pages and scroll position. | |
| queryClient.invalidateQueries({ | |
| queryKey: ["user", "conversations"], | |
| }); | |
| // The cloud path returns a start task (no app_conversation_id | |
| // yet); the sidebar surfaces those via `useStartTasks` which doesn't | |
| // poll, so invalidate it explicitly so the in-flight task shows up | |
| // in the conversation list immediately. | |
| queryClient.invalidateQueries({ | |
| queryKey: ["start-tasks"], | |
| }); | |
| }, | |
| }); | |
| }; | |