refactor(api): use typescript-client ConversationClient + FileClient instead of hand-written HTTP paths#291
Conversation
…instead of hand-written HTTP paths
The typescript-client already ships fully-typed `ConversationClient` and
`FileClient` covering every endpoint that
`AgentServerConversationService` and `FilesService` were calling by
hand with raw `createHttpClient().{get,post,patch,delete}`. We were
duplicating ~12 endpoint paths with looser types than the SDK already
provides.
This refactor pushes the duplication back to its source of truth:
- `AgentServerConversationService.sendMessage` →
`ConversationClient.sendEvent(id, msg, { run: true })`
- `createConversation` (local) → `ConversationClient.createConversation`
- `pauseConversation` / `resumeConversation` / `askAgent` →
`ConversationClient.{pauseConversation, runConversation, askAgent}`
- `batchGetAppConversations` → `ConversationClient.getConversations(ids)`
- `searchConversations` (local) → `ConversationClient.searchConversations`
(with the SDK's `ConversationSortOrder.UPDATED_AT_DESC` enum)
- `deleteConversation` (local) → `ConversationClient.deleteConversation`
- `updateConversationTitle` → `ConversationClient.updateConversation`
- `getRuntimeConversation` (local branch) →
`ConversationClient.getConversation`
- `downloadConversation` (local) → `FileClient.downloadTrajectory`
- `agent-server-adapter.downloadTextFile` → `FileClient.downloadTextFile`
- `FilesService.{searchSubdirs,getHome}` →
`FileClient.{searchSubdirectories,getHome}`
Two thin factories on top of `resolveClientOptions` were added to
`src/api/typescript-client.ts` (`createConversationClient` and
`createFileClient`) so call sites pick up the active backend's host +
`X-Session-API-Key` the same way `createHttpClient` does. They use the
same `TypeScriptClientOverrides` shape (`{ sessionApiKey, conversationUrl }`)
so per-call key/host overrides keep working unchanged.
The one local-mode `searchConversations` call site casts
`ConversationInfo[]` → `DirectConversationInfo[]` through `unknown` —
honest about the narrow-type swap (the JSON shape is identical on the
local agent-server; agent-canvas's `DirectConversationInfo` is just the
subset of fields it reads). The comment explains why.
Tests updated:
- `agent-server-conversation-service.test.ts` now mocks
`createConversationClient` / `createFileClient` returning typed-client
shapes rather than the old `createHttpClient` `{get, post, ...}` mock.
- `use-create-conversation-metadata.test.ts` same.
- Both keep a `createHttpClient` stub returning `{secrets: []}` because
`SecretsService.getSecrets` is still on the raw `createHttpClient`
path (flagged as follow-up in the PR description) and its retry +
fallback would otherwise stall the mutation under `waitFor`.
Verification:
- `npm run typecheck` clean
- `npm run lint` — 0 errors, same 10 pre-existing warnings
- `npx vitest run` — 1862 passed | 12 skipped | 9 todo (no regressions
vs main; +3 new test states since the suite has gained tests in
parallel branches that exercise the new factories transitively)
Co-authored-by: openhands <openhands@all-hands.dev>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ing raw createHttpClient call sites Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste Rating: 🟢 Good taste - Clean refactor that improves type safety by replacing hand-written HTTP calls with typed SDK clients.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟢 LOW
Internal refactoring only. No user-facing changes, no agent behavior changes, no eval impact. The SDK clients (ConversationClient, FileClient) wrap the same HTTP endpoints with proper types. The two casts (payload as Record<string, unknown> and ConversationInfo[] as unknown as DirectConversationInfo[]) are explained and safe. Tests updated and passing (1862 passed).
VERDICT:
✅ Worth merging - Improves type safety and reduces hand-written HTTP boilerplate without changing behavior.
KEY INSIGHT:
This PR demonstrates the right way to adopt SDK clients: migrate incrementally (12 call sites), keep behavior identical, explain necessary casts, and flag remaining work (SecretsService) for follow-up.
|
@OpenHands fix failing gh action |
|
I'm on it! rbren can track my progress at all-hands.dev |
Co-authored-by: openhands <openhands@all-hands.dev>
|
OpenHands encountered an error: Request timeout after 30 seconds to https://sjvnzhqjmpxwzjfi.prod-runtime.all-hands.dev/api/conversations/8837e85a-68bd-4938-9670-68a792025ffe/ask_agent See the conversation for more information. |
Co-authored-by: openhands <openhands@all-hands.dev>
What
Pushes 12 hand-written
createHttpClient().{get,post,patch,delete}call sites inAgentServerConversationService/FilesService/agent-server-adapterdown to the typedConversationClientandFileClientthat the typescript-client SDK already ships. No new endpoints, no new request shapes — agent-canvas was duplicating the SDK by hand with looser types.Why
Today, every method in
AgentServerConversationServicewrites the URL string literal itself (e.g.`/api/conversations/${id}/pause`) and types the response inline ({success: boolean}). The SDK has:ConversationClient.{createConversation, getConversation, getConversations, searchConversations, sendEvent, pauseConversation, runConversation, askAgent, updateConversation, deleteConversation, …}FileClient.{getHome, searchSubdirectories, downloadFile, downloadTextFile, downloadTrajectory}…all already imported transitively through
@openhands/typescript-client/clients, with proper request/response types inmodels/conversation.tsandmodels/api.ts. So the right fix is to use them.Mapping
createHttpClient().post('/api/conversations/:id/events', {...msg, run:true})ConversationClient.sendEvent(id, msg, { run: true })createHttpClient().post<DirectConversationInfo>('/api/conversations', payload)ConversationClient.createConversation<DirectConversationInfo>(payload)createHttpClient({sessionApiKey}).post('/api/conversations/:id/pause', {})ConversationClient.pauseConversation(id)(with{sessionApiKey})createHttpClient({sessionApiKey}).post('/api/conversations/:id/run', {})ConversationClient.runConversation(id)createHttpClient({sessionApiKey}).post('/api/conversations/:id/ask_agent', {q})ConversationClient.askAgent(id, q)createHttpClient().get('/api/conversations', { params: { ids } })ConversationClient.getConversations<DirectConversationInfo>(ids)createHttpClient().get('/api/conversations/search', { params: … })ConversationClient.searchConversations({limit, page_id, sort_order: ConversationSortOrder.UPDATED_AT_DESC})createHttpClient().delete('/api/conversations/:id')ConversationClient.deleteConversation(id)createHttpClient().patch('/api/conversations/:id', {title})ConversationClient.updateConversation(id, {title})createHttpClient({conversationUrl, sessionApiKey}).get('/api/conversations/:id')ConversationClient.getConversation<RawRuntime>(id)(same overrides)createHttpClient().get<Blob>('/api/file/download-trajectory/:id', {responseType:'blob'})FileClient.downloadTrajectory(id)createHttpClient().get<ArrayBuffer>('/api/file/download', { params: {path}, responseType:'arrayBuffer' })FileClient.downloadTextFile(path)createHttpClient().get('/api/file/search_subdirs', { params: … })FileClient.searchSubdirectories(path, {pageId, limit})createHttpClient().get('/api/file/home')FileClient.getHome()New factories
Two thin wrappers on top of the existing
resolveClientOptions(overrides)helper insrc/api/typescript-client.ts:They take the same
{ sessionApiKey, conversationUrl, … }shape ascreateHttpClient, so per-call host / session-key overrides keep working exactly the same (used bypauseConversation,getRuntimeConversation, etc.).Notes
searchConversationscastsConversationInfo[](SDK type) →DirectConversationInfo[](agent-canvas's narrower view of the same JSON) throughunknown. The comment in-file explains why; both types describe the same agent-server response, just different reads of it.callCloudProxy(because cross-origin to*.prod-runtime.all-hands.devdoesn't work fromlocalhost); those paths are untouched.SecretsService.getSecretsis still on rawcreateHttpClient. The retry + fallback-to-empty-array pattern there would interact awkwardly with this PR's test mocks if migrated in the same patch. Flagged as follow-up below.Verification
Two test files needed mock updates — both now mock the new client factories with typed shapes instead of the old
{get, post, patch, delete}http-client mock. Same coverage, but the assertions read more naturally (expect(mockDownloadTrajectory).toHaveBeenCalledWith("conv-abc")vs.expect(mockHttpGet).toHaveBeenCalledWith("/api/file/download-trajectory/conv-abc", { responseType: "blob" })).Additional findings (not addressed in this PR)
From the broader code-review pass on this repo, these are worth follow-up PRs (decreasing order of value):
SecretsService(src/api/secrets-service.ts) andcloud/organization-service.api.tsare still on rawcreateHttpClient. Same pattern — the SDK has neither aSecretsClientnor anOrganizationClientyet, so this would be a typescript-client PR first. Worth doing because both surfaces would benefit from typed responses.RemoteWorkspace.src/api/git-service/agent-server-git-service.api.tshand-routes/api/git/{changes,diff}throughcallCloudProxybecause the SDK'sRemoteWorkspace.gitChanges/gitDiffalways talks directly to its configured host (which CORS-fails against*.prod-runtime.all-hands.devfrom a browser). A genuine SDK-side feature would be aRemoteWorkspaceconstructor option that takes a transport callback (or aCloudRemoteWorkspacesubclass), letting the SDK do path normalization + auth uniformly across both modes. This is the next-substantive PR I'd recommend.toAbsoluteRuntimePathworkaround inagent-server-git-service.api.ts(the cloud runtime double-prepends/workspace/to relative paths). Should be fixed on the runtime side. Currently the workaround lives in two places (getGitChanges,getGitDiff) — fine, but worth a TODO link to whatever runtime issue tracks the dupe-prefix bug.TOS$ACCEPT, etc.) appear insrc/i18n/translation.jsonwith not(I18nKey.…)consumers. A dead-string sweep could quietly remove them.environment: 406sdominating; suggests cost ofhappy-domsetup. Worth experimenting with shared environments /pool: 'threads'on CI.src/components/features/file-explorer/file-diff-viewer.tsx:240collapse, the refresh button I already fixed in chore: address post-merge feedback on #284 + small code-review fixes #288, and a few<button>insrc/components/features/conversation/…). A targetedaxe-driven pass would be cheap and high-ROI.