Skip to content

refactor(api): use typescript-client ConversationClient + FileClient instead of hand-written HTTP paths#291

Open
rbren wants to merge 5 commits into
mainfrom
refactor/use-sdk-conversation-and-file-clients
Open

refactor(api): use typescript-client ConversationClient + FileClient instead of hand-written HTTP paths#291
rbren wants to merge 5 commits into
mainfrom
refactor/use-sdk-conversation-and-file-clients

Conversation

@rbren
Copy link
Copy Markdown
Member

@rbren rbren commented May 11, 2026

What

Pushes 12 hand-written createHttpClient().{get,post,patch,delete} call sites in AgentServerConversationService / FilesService / agent-server-adapter down to the typed ConversationClient and FileClient that the typescript-client SDK already ships. No new endpoints, no new request shapes — agent-canvas was duplicating the SDK by hand with looser types.

This PR was opened by an AI agent (OpenHands) on behalf of @rbren. Companion to #288 / #290.

Why

Today, every method in AgentServerConversationService writes 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 in models/conversation.ts and models/api.ts. So the right fix is to use them.

Mapping

Before (raw HTTP) After (typed client)
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 in src/api/typescript-client.ts:

export function createConversationClient(overrides?: TypeScriptClientOverrides): ConversationClient
export function createFileClient(overrides?: TypeScriptClientOverrides): FileClient

They take the same { sessionApiKey, conversationUrl, … } shape as createHttpClient, so per-call host / session-key overrides keep working exactly the same (used by pauseConversation, getRuntimeConversation, etc.).

Notes

  • One cast, explained. Local-mode searchConversations casts ConversationInfo[] (SDK type) → DirectConversationInfo[] (agent-canvas's narrower view of the same JSON) through unknown. The comment in-file explains why; both types describe the same agent-server response, just different reads of it.
  • Cloud paths unchanged. Every method's cloud branch already goes through callCloudProxy (because cross-origin to *.prod-runtime.all-hands.dev doesn't work from localhost); those paths are untouched.
  • SecretsService.getSecrets is still on raw createHttpClient. 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

$ npm run typecheck                # passes
$ npm run lint                     # 0 errors, same 10 pre-existing warnings
$ npx vitest run                   # 286 files | 1862 passed | 12 skipped | 9 todo

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):

  1. SecretsService (src/api/secrets-service.ts) and cloud/organization-service.api.ts are still on raw createHttpClient. Same pattern — the SDK has neither a SecretsClient nor an OrganizationClient yet, so this would be a typescript-client PR first. Worth doing because both surfaces would benefit from typed responses.
  2. Cloud-mode RemoteWorkspace. src/api/git-service/agent-server-git-service.api.ts hand-routes /api/git/{changes,diff} through callCloudProxy because the SDK's RemoteWorkspace.gitChanges/gitDiff always talks directly to its configured host (which CORS-fails against *.prod-runtime.all-hands.dev from a browser). A genuine SDK-side feature would be a RemoteWorkspace constructor option that takes a transport callback (or a CloudRemoteWorkspace subclass), letting the SDK do path normalization + auth uniformly across both modes. This is the next-substantive PR I'd recommend.
  3. toAbsoluteRuntimePath workaround in agent-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.
  4. Translations cleanup. Several leftover keys from the OSS cleanup (TOS$ACCEPT, etc.) appear in src/i18n/translation.json with no t(I18nKey.…) consumers. A dead-string sweep could quietly remove them.
  5. Vitest run time. ~5.5 min for 1862 tests with environment: 406s dominating; suggests cost of happy-dom setup. Worth experimenting with shared environments / pool: 'threads' on CI.
  6. Accessibility sweep. Quick grep finds several icon-only buttons (src/components/features/file-explorer/file-diff-viewer.tsx:240 collapse, the refresh button I already fixed in chore: address post-merge feedback on #284 + small code-review fixes #288, and a few <button> in src/components/features/conversation/…). A targeted axe-driven pass would be cheap and high-ROI.

…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>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-canvas Error Error May 12, 2026 2:28am

Request Review

…ing raw createHttpClient call sites

Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Copy Markdown
Contributor

@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.

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.

@rbren
Copy link
Copy Markdown
Member Author

rbren commented May 11, 2026

@OpenHands fix failing gh action

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 11, 2026

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

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented May 12, 2026

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>
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.

3 participants