import { xhrFetch, type XhrResponse } from './xhrFetch'; import type { QueuedSuperagentMessage, SuperagentConversation, SuperagentCurrentUsage, SuperagentMessage, SuperagentNativeClient, SuperagentNativeClientConfig, SuperagentPaginatedMessages, SuperagentPromptSuggestion, } from '../types'; type QueryValue = string | number | boolean | undefined; export function createSuperagentNativeClient({ apiBaseUrl, appId, getAuthToken, getHeaders, }: SuperagentNativeClientConfig): SuperagentNativeClient { const executeRequest = async ( method: string, url: string, options: { body?: unknown; headers?: Record } = {}, ): Promise => { const token = await getAuthToken(); const extraHeaders = await getHeaders?.(); const response = await xhrFetch(url, { method, headers: { ...(options.body === undefined ? {} : { 'Content-Type': 'application/json' }), ...(token ? { Authorization: `Bearer ${token}` } : {}), ...extraHeaders, ...options.headers, }, body: options.body === undefined ? undefined : JSON.stringify(options.body), }); if (!response.ok) { throw await createClientError(response); } if (response.status === 204) { return undefined as TResponse; } return response.json() as Promise; }; const request = ( method: string, path: string, options: { body?: unknown; query?: Record; headers?: Record; } = {}, ): Promise => executeRequest(method, buildUrl(apiBaseUrl, path, options.query), { body: options.body, headers: options.headers, }); const basePath = `/${pathSegment(appId, 'appId')}/user-agent/conversations`; // /api/usage-logs is a sibling of this client's /api/apps base; derive it rather than // threading a second URL through the config. const usageLogsBaseUrl = `${apiBaseUrl.replace(/\/apps\/?$/, '')}/usage-logs`; return { getConversations(limit = 1) { return request('GET', basePath, { query: { sort: '-updated_date', limit }, }); }, getConversation(conversationId) { return request('GET', `${basePath}/${pathSegment(conversationId, 'conversationId')}`); }, getMessages(conversationId, params = {}) { return request('GET', `${basePath}/${pathSegment(conversationId, 'conversationId')}/messages`, { query: params, }); }, createConversation(metadata = {}) { return request('POST', basePath, { body: { metadata } }); }, bootstrapIntro(conversationId, messageParams = {}) { return request<{ started: boolean }>( 'POST', `${basePath}/${pathSegment(conversationId, 'conversationId')}/bootstrap-intro`, { body: { message_params: messageParams } }, ); }, addMessage(conversationId, message) { return request('POST', `${basePath}/${pathSegment(conversationId, 'conversationId')}/messages`, { body: serializeMessageForBackend(message), }); }, stopConversation(conversationId) { return request('POST', `${basePath}/${pathSegment(conversationId, 'conversationId')}/stop`); }, getQueuedMessages(conversationId) { return request<{ messages: QueuedSuperagentMessage[] }>( 'GET', `${basePath}/${pathSegment(conversationId, 'conversationId')}/queued-messages`, ); }, editQueuedMessage(conversationId, messageId, content) { return request<{ ok: boolean }>( 'PUT', `${basePath}/${pathSegment(conversationId, 'conversationId')}/queued-messages/${pathSegment(messageId, 'messageId')}`, { body: { content } }, ); }, deleteQueuedMessage(conversationId, messageId) { return request<{ ok: boolean }>( 'DELETE', `${basePath}/${pathSegment(conversationId, 'conversationId')}/queued-messages/${pathSegment(messageId, 'messageId')}`, ); }, deleteMessage(conversationId, messageId) { return request<{ message: string }>( 'DELETE', `${basePath}/${pathSegment(conversationId, 'conversationId')}/messages/${pathSegment(messageId, 'messageId')}`, ); }, submitToolCallInput(conversationId, toolCallId, approve, extraUserInput, originRequestId) { return request( 'POST', `${basePath}/${pathSegment(conversationId, 'conversationId')}/submit-tool-call-input`, { body: { tool_call_id: toolCallId, action: approve ? 'approved' : 'rejected', extra_user_input: extraUserInput, }, headers: originRequestId ? { 'X-Request-ID': originRequestId } : undefined, }, ); }, getPromptSuggestions(options = {}) { const query: Record = {}; if (options.refresh) query.refresh = true; // The backend expects a single `|`-joined string of already-shown titles. if (options.exclude?.length) query.exclude = options.exclude.join('|'); if (options.conversationId) query.conversation_id = options.conversationId; return request<{ suggestions: SuperagentPromptSuggestion[] }>( 'GET', `/${pathSegment(appId, 'appId')}/user-agent/prompt-suggestions`, { query }, ); }, getCurrentUsage() { // The backend bills /current-usage against the active-workspace context, which it // resolves from X-Active-App-Id → the app's workspace; the app_id query is only the // staff temp-access redirect. Send the header so the read hits the agent's workspace // bucket (not the caller's personal org) for team/shared-workspace agents. return executeRequest( 'GET', buildUrl(`${usageLogsBaseUrl}/current-usage`, '', { app_id: appId }), { headers: { 'X-Active-App-Id': appId } }, ); }, }; } function buildUrl(baseUrl: string, path: string, query?: Record) { const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; const url = `${normalizedBase}${path}`; const queryString = buildQueryString(query); return queryString ? `${url}?${queryString}` : url; } function buildQueryString(query?: Record) { return Object.entries(query ?? {}) .filter(([, value]) => value !== undefined) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) .join('&'); } function pathSegment(value: string, name: string) { if (!value || value.includes('\0')) { throw new Error(`Invalid ${name}`); } return encodeURIComponent(value); } async function createClientError(response: XhrResponse) { let message = `Request failed with status ${response.status}`; try { const body = await response.json() as { message?: unknown; detail?: unknown }; if (typeof body?.message === 'string') message = body.message; else if (typeof body?.detail === 'string') message = body.detail; } catch { // Keep the status-based fallback message. } const error = new Error(message) as Error & { status?: number }; error.status = response.status; return error; } function serializeMessageForBackend(message: Partial) { const { createdAt, created_at, fileUrls, file_urls, replyTo, toolCalls, tool_calls, ...backendMessage } = message; const additionalParams = { ...backendMessage.additional_message_params, ...(replyTo ? { reply_to: { message_id: replyTo.messageId, content: replyTo.content } } : {}), }; return omitUndefined({ ...backendMessage, file_urls: file_urls ?? fileUrls, tool_calls: tool_calls ?? toolCalls, additional_message_params: Object.keys(additionalParams).length > 0 ? additionalParams : undefined, }); } function omitUndefined>(value: T) { return Object.fromEntries( Object.entries(value).filter(([, entry]) => entry !== undefined), ); } export type { SuperagentNativeClient };