@dudousxd/nestjs-agent-react 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/{client-C7eu9WMJ.d.cts → client-BdHUuqy-.d.cts} +36 -1
- package/dist/{client-C7eu9WMJ.d.ts → client-BdHUuqy-.d.ts} +36 -1
- package/dist/{generative-ui-VTZ1EADD.d.cts → generative-ui-CyHY8Lh1.d.cts} +32 -101
- package/dist/{generative-ui-VTZ1EADD.d.ts → generative-ui-DdUZPCXX.d.ts} +32 -101
- package/dist/genui-json-render.cjs +51 -8
- package/dist/genui-json-render.cjs.map +1 -1
- package/dist/genui-json-render.d.cts +2 -1
- package/dist/genui-json-render.d.ts +2 -1
- package/dist/genui-json-render.js +51 -8
- package/dist/genui-json-render.js.map +1 -1
- package/dist/genui-server-playwright.cjs +199 -0
- package/dist/genui-server-playwright.cjs.map +1 -0
- package/dist/genui-server-playwright.d.cts +18 -0
- package/dist/genui-server-playwright.d.ts +18 -0
- package/dist/genui-server-playwright.js +162 -0
- package/dist/genui-server-playwright.js.map +1 -0
- package/dist/genui-server.cjs +188 -0
- package/dist/genui-server.cjs.map +1 -0
- package/dist/genui-server.d.cts +36 -0
- package/dist/genui-server.d.ts +36 -0
- package/dist/genui-server.js +163 -0
- package/dist/genui-server.js.map +1 -0
- package/dist/genui.cjs +127 -8
- package/dist/genui.cjs.map +1 -1
- package/dist/genui.d.cts +5 -2
- package/dist/genui.d.ts +5 -2
- package/dist/genui.js +128 -8
- package/dist/genui.js.map +1 -1
- package/dist/index.cjs +559 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -10
- package/dist/index.d.ts +35 -10
- package/dist/index.js +552 -120
- package/dist/index.js.map +1 -1
- package/dist/media.cjs.map +1 -1
- package/dist/media.d.cts +1 -1
- package/dist/media.d.ts +1 -1
- package/dist/media.js.map +1 -1
- package/dist/react-registry-Di4IVrw-.d.cts +25 -0
- package/dist/react-registry-z9GFbTm9.d.ts +25 -0
- package/dist/types-DZtFsWec.d.cts +99 -0
- package/dist/types-DZtFsWec.d.ts +99 -0
- package/package.json +35 -5
package/dist/media.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/media/index.ts","../src/http-error.ts","../src/client.ts"],"sourcesContent":["import type { MessageAttachment } from '@dudousxd/nestjs-agent-core';\nimport { streamChunks } from '@dudousxd/nestjs-media-client';\nimport type { AttachmentUploadStrategy, UploadAttachmentOptions } from '../backend.js';\nimport { type AgentClientOptions, normalizeAgentPath } from '../client.js';\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from '../http-error.js';\n\n/** Tuning for {@link mediaAttachments}; every field optional. */\nexport interface MediaAttachmentsOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB. */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/**\n * Resumable attachment uploads through `@dudousxd/nestjs-media`, in one line:\n *\n * ```tsx\n * <AgentProvider attachments={{ upload: mediaAttachments() }}>\n * ```\n *\n * Reuses the client's own connection (origin, path, headers, credentials), so it needs no\n * configuration. Also takes `new AgentClient({ attachments: { upload: mediaAttachments() } })`.\n */\nexport function mediaAttachments(options: MediaAttachmentsOptions = {}): AttachmentUploadStrategy {\n return (file, uploadOptions, connection) =>\n createMediaUpload({\n ...options,\n baseUrl: connection.baseUrl,\n path: connection.path,\n getHeaders: connection.headers,\n fetch: connection.fetch,\n ...(connection.credentials !== undefined ? { credentials: connection.credentials } : {}),\n ...(connection.onHttpError !== undefined ? { onHttpError: connection.onHttpError } : {}),\n })(file, uploadOptions);\n}\n\n/**\n * Where and how {@link createMediaUpload} talks to the server. The connection fields are\n * {@link AgentClientOptions}' — pass the same ones your `AgentClient` gets, so the tus requests\n * carry the same cookies / CSRF header / bearer token as the rest of the chat.\n */\nexport interface MediaUploadOptions extends AgentClientOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB (nestjs-media-client's default). */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/** What `useAttachments({ upload })` and `AgentBackend.uploadAttachment` both take. */\nexport type AttachmentUpload = (\n file: File,\n options?: UploadAttachmentOptions,\n) => Promise<MessageAttachment>;\n\n/**\n * A non-2xx answer from the agent's upload routes. Carries what `AgentHttpError` does — `status`\n * (413 too large, 415 type refused, 404 not yours), the server's `message`, `code` and `body` —\n * as its own class because this subpath is bundled apart from the root entry, where an\n * `instanceof AgentHttpError` would not match a copy. Both satisfy `AgentRequestError`.\n */\nexport class MediaUploadError extends Error implements AgentRequestError {\n readonly body: unknown;\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(\n answer.message ?? `Attachment upload failed: ${method} ${path} → ${status} ${statusText}`,\n );\n this.name = 'MediaUploadError';\n this.body = answer.body;\n this.code = answer.code;\n }\n}\n\ninterface BeginResponse {\n mediaId: string;\n location: string;\n}\n\n/**\n * A resumable `upload` for `useAttachments`, backed by `@dudousxd/nestjs-media` on the server\n * (`AgentMediaAttachmentsModule` from `@dudousxd/nestjs-agent/media`):\n *\n * 1. `POST <path>/attachments/uploads` — the agent validates the file and opens a tus session the\n * actor owns;\n * 2. the bytes stream to nestjs-media's own tus endpoint in chunks (`streamChunks`), reporting\n * progress and honouring the abort signal;\n * 3. `POST <path>/attachments/uploads/:mediaId/complete` — the agent confirms the bytes landed and\n * answers the `MessageAttachment` the turn will reference by `mediaId`.\n *\n * An aborted or failed upload is discarded server-side (`DELETE`), best effort.\n */\nexport function createMediaUpload(options: MediaUploadOptions = {}): AttachmentUpload {\n const origin = (options.baseUrl ?? '').replace(/\\/$/, '');\n const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;\n const baseFetch = (): typeof fetch => options.fetch ?? fetch;\n // Every request — the agent's and media's tus PATCHes — rides the same credentials mode.\n const fetchWithCredentials: typeof fetch = (input, init) =>\n baseFetch()(input, {\n ...init,\n ...(options.credentials !== undefined ? { credentials: options.credentials } : {}),\n });\n const headers = async (): Promise<Record<string, string>> => ({\n ...options.headers,\n ...((await options.getHeaders?.()) ?? {}),\n });\n const discard = (mediaId: string): void => {\n void (async () => {\n await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {\n method: 'DELETE',\n headers: await headers(),\n });\n })().catch(() => undefined);\n };\n const call = async <T>(method: string, url: string, body?: unknown): Promise<T> => {\n const response = await fetchWithCredentials(url, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await headers()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n if (!response.ok) {\n const error = new MediaUploadError(\n response.status,\n method,\n url,\n response.statusText,\n await readErrorResponse(response),\n );\n reportHttpError(options.onHttpError, error);\n throw error;\n }\n return (await response.json()) as T;\n };\n\n return async (file, { signal, onProgress } = {}) => {\n signal?.throwIfAborted();\n const begun = await call<BeginResponse>('POST', base, {\n filename: file.name,\n contentType: file.type,\n size: file.size,\n });\n const onAbort = () => discard(begun.mediaId);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n const location = /^https?:\\/\\//.test(begun.location)\n ? begun.location\n : `${origin}${begun.location}`;\n await streamChunks(location, file, {\n resume: false,\n fetchImpl: fetchWithCredentials,\n getHeaders: headers,\n ...(options.chunkSize !== undefined ? { chunkSize: options.chunkSize } : {}),\n ...(options.retries !== undefined ? { retries: options.retries } : {}),\n ...(signal !== undefined ? { signal } : {}),\n onProgress: (sent, total) => onProgress?.(total > 0 ? sent / total : 1),\n });\n signal?.throwIfAborted();\n const attachment = await call<MessageAttachment>(\n 'POST',\n `${base}/${encodeURIComponent(begun.mediaId)}/complete`,\n );\n onProgress?.(1);\n return attachment;\n } catch (error) {\n if (!signal?.aborted) discard(begun.mediaId);\n throw error;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n","/**\n * What a server said when it refused a request, read once so every error class of this package\n * reports it the same way. Bundled into each entry that throws (the root and `/media`), which is\n * why it holds no class of its own.\n */\nexport interface ErrorAnswer {\n /** The body, parsed as JSON when it is JSON, else the raw text; `undefined` when empty. */\n body: unknown;\n /** `body.message` — a string, or NestJS's list of validation messages joined with `; `. */\n message: string | undefined;\n /** `body.code`, the machine-readable reason (`quota_exceeded`, …), when the server sent one. */\n code: string | undefined;\n}\n\n/** Read an error answer's body text. Never throws: a body that cannot be read is just absent. */\nexport function readErrorAnswer(text: string | null | undefined): ErrorAnswer {\n if (text == null || text === '') return { body: undefined, message: undefined, code: undefined };\n let body: unknown = text;\n try {\n body = JSON.parse(text);\n } catch {\n return { body: text, message: undefined, code: undefined };\n }\n if (body === null || typeof body !== 'object')\n return { body, message: undefined, code: undefined };\n const record = body as { message?: unknown; code?: unknown };\n const message =\n typeof record.message === 'string' && record.message !== ''\n ? record.message\n : Array.isArray(record.message) &&\n record.message.length > 0 &&\n record.message.every((each) => typeof each === 'string')\n ? record.message.join('; ')\n : undefined;\n const code = typeof record.code === 'string' && record.code !== '' ? record.code : undefined;\n return { body, message, code };\n}\n\n/** {@link readErrorAnswer} over a `Response` whose status is not 2xx. */\nexport async function readErrorResponse(response: Response): Promise<ErrorAnswer> {\n let text: string | undefined;\n try {\n text = await response.text();\n } catch {\n text = undefined;\n }\n return readErrorAnswer(text);\n}\n\n/**\n * The shape both {@link import('./client.js').AgentHttpError} and `MediaUploadError` share, for\n * code that handles either without an `instanceof` (the two live in separate bundles).\n */\nexport interface AgentRequestError extends Error {\n status: number;\n method: string;\n path: string;\n body: unknown;\n code: string | undefined;\n}\n\n/** Called with every error answer before it is thrown — for app-wide reactions (401, 402, …). */\nexport type HttpErrorListener = (error: AgentRequestError) => void;\n\n/** Tell `listener` about `error`; a listener that throws never replaces the error itself. */\nexport function reportHttpError(\n listener: HttpErrorListener | undefined,\n error: AgentRequestError,\n): void {\n if (listener === undefined) return;\n try {\n listener(error);\n } catch {\n /* the request's own error is what the caller gets */\n }\n}\n","import type {\n AgentCatalogEntry,\n AgentClientConfig,\n ChatQueueState,\n MessageAttachment,\n MessageFeedback,\n ModelCatalogView,\n QuotaReport,\n SkillCatalogEntry,\n ThreadDetail,\n ThreadSummary,\n ToolCatalogEntry,\n} from '@dudousxd/nestjs-agent-core';\nimport type {\n AgentBackend,\n AgentConnection,\n AttachmentUploadStrategy,\n ChatStreamRequest,\n ChatStreamResponse,\n MessageFeedbackInput,\n QueuedMessageUpdate,\n QueuedSendResult,\n ResumeStreamRequest,\n ThreadPatch,\n UploadAttachmentOptions,\n} from './backend.js';\n\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n type HttpErrorListener,\n readErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from './http-error.js';\n\nexport type { ThreadPatch } from './backend.js';\nexport type { AgentRequestError, HttpErrorListener } from './http-error.js';\n\n/**\n * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can\n * branch (e.g. 403 → \"not your thread\", 429 → quota) instead of string-matching a generic Error,\n * and what the server said: `message` is the body's `message` when it sent one (so a hook that\n * shows `error.message` shows the server's words), `code` its machine-readable `code`, and `body`\n * the whole answer, parsed when it is JSON.\n */\nexport class AgentHttpError extends Error implements AgentRequestError {\n /** The answer's body: parsed JSON, else its text; `undefined` when empty. */\n readonly body: unknown;\n /** The body's `code` (`quota_exceeded`, …), when the server sent one. */\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(answer.message ?? `Agent request failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'AgentHttpError';\n this.body = answer.body;\n this.code = answer.code;\n }\n\n /** Read a refused `response`'s body into an error. */\n static async from(response: Response, method: string, path: string): Promise<AgentHttpError> {\n return new AgentHttpError(\n response.status,\n method,\n path,\n response.statusText,\n await readErrorResponse(response),\n );\n }\n}\n\nexport interface CancelResult {\n aborted: boolean;\n}\n\nexport interface OkResult {\n ok: boolean;\n}\n\nexport interface AgentClientOptions {\n /**\n * The server's origin, e.g. `https://api.example.com`. Defaults to `''` (same origin). The\n * agent's route prefix is {@link AgentClientOptions.path}, not part of this.\n */\n baseUrl?: string;\n /**\n * The agent's route prefix — `AgentModule`'s `path`, with any global prefix in front\n * (`'api/agent'`). Leading/trailing slashes are optional. Defaults to `'agent'`.\n */\n path?: string;\n /** Static headers merged into every request. */\n headers?: Record<string, string>;\n /**\n * Resolved per request — for short-lived bearer tokens, or a CSRF header read from a cookie\n * (`{ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }`), which has to be read at request time because\n * the server may rotate it.\n */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /**\n * Forwarded to fetch so cookie auth works. Same-origin requests send cookies by default; set\n * `'include'` when the API lives on another origin (and have it answer with credentialed CORS).\n */\n credentials?: RequestCredentials;\n /** Injectable for tests / non-browser runtimes. */\n fetch?: typeof fetch;\n /**\n * Called with every error answer (an {@link AgentHttpError}, or a `MediaUploadError` from a\n * `mediaAttachments()` upload) right before it is thrown — the place for app-wide reactions such\n * as \"401 → sign in again\". The error still reaches the caller. Not called for the `404` a\n * resume answers when nothing is streaming, which is an answer, not a failure.\n */\n onHttpError?: HttpErrorListener;\n /** Attachment uploads. */\n attachments?: {\n /**\n * How `uploadAttachment` uploads. Omitted → `POST <path>/attachments` (multipart). Pass\n * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through\n * nestjs-media, or your own {@link AttachmentUploadStrategy}.\n */\n upload?: AttachmentUploadStrategy;\n };\n}\n\n/** `'/api/agent'` from `'api/agent'`, `'/api/agent/'`, …; `''` for an empty path. */\nexport function normalizeAgentPath(path: string | undefined): string {\n const trimmed = (path ?? 'agent').replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `/${trimmed}`;\n}\n\nconst HEADER_RUN_ID = 'x-agent-run-id';\nconst HEADER_THREAD_ID = 'x-agent-thread-id';\n\n/**\n * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.\n * Used by `useAgentChat`, but standalone-usable (vanilla fetch, no React).\n */\nexport class AgentClient implements AgentBackend {\n constructor(private readonly options: AgentClientOptions = {}) {}\n\n /** `POST <path>/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */\n async openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse> {\n const response = await this.fetchImpl()(`${this.root()}/chat`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n body: JSON.stringify(request.body),\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 202) {\n // Queued behind a turn already running on the thread: JSON, not a stream.\n return { body: emptyStream(), queued: (await response.json()) as QueuedSendResult };\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'POST', `${this.agentPath()}/chat`);\n }\n return streamResponse(response);\n }\n\n /**\n * `POST <path>/chat` for a message that should wait in the thread's queue — the body carries\n * `mode: 'queue'` (or `'interrupt'`), which the server always answers with `202` JSON.\n */\n enqueueMessage(request: ChatStreamRequest): Promise<QueuedSendResult> {\n return this.request<QueuedSendResult>('POST', '/chat', {\n mode: 'queue',\n ...request.body,\n });\n }\n\n /** `GET <path>/threads/:id/queue` — the thread's waiting messages, and whether it drains. */\n getQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('GET', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `PATCH <path>/queue/:messageId` — change a waiting message's text/attachments, or move it. */\n updateQueuedMessage(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('PATCH', `/queue/${encodeURIComponent(messageId)}`, update);\n }\n\n /** `DELETE <path>/queue/:messageId`. */\n removeQueuedMessage(messageId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/queue/${encodeURIComponent(messageId)}`);\n }\n\n /**\n * `POST <path>/queue/:messageId/interrupt` — run a waiting message now, cancelling the running\n * turn for it. `runId` when nothing was running and it started; else `interrupting`.\n */\n interruptQueuedMessage(\n messageId: string,\n ): Promise<ChatQueueState & { runId?: string; interrupting?: string }> {\n return this.request<ChatQueueState & { runId?: string; interrupting?: string }>(\n 'POST',\n `/queue/${encodeURIComponent(messageId)}/interrupt`,\n );\n }\n\n /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */\n clearQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */\n resumeQueue(threadId: string): Promise<ChatQueueState & { runId?: string }> {\n return this.request<ChatQueueState & { runId?: string }>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/queue/resume`,\n );\n }\n\n /**\n * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is\n * streaming under that id (404).\n */\n async resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null> {\n const path = `/chat/${encodeURIComponent(request.runId)}/stream`;\n const query = request.after !== undefined && request.after > 0 ? `?after=${request.after}` : '';\n const response = await this.fetchImpl()(`${this.root()}${path}${query}`, {\n method: 'GET',\n headers: {\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 404) {\n return null;\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'GET', `${this.agentPath()}${path}`);\n }\n return streamResponse(response);\n }\n\n /**\n * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers\n * the stored rating.\n */\n setMessageFeedback(\n messageId: string,\n input: MessageFeedbackInput,\n ): Promise<{ feedback: MessageFeedback | null }> {\n return this.request<{ feedback: MessageFeedback | null }>(\n 'POST',\n `/messages/${encodeURIComponent(messageId)}/feedback`,\n input,\n );\n }\n\n listThreads(): Promise<ThreadSummary[]> {\n return this.request<ThreadSummary[]>('GET', '/threads');\n }\n\n /**\n * The skills this caller can invoke right now, scope-resolved — the same list, built by the same\n * call, that the model is offered, so what a user can type after a `/` and what the agent can\n * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to\n * one conversation; omitted, the server reads it as a brand-new thread.\n */\n listSkills(threadId?: string): Promise<SkillCatalogEntry[]> {\n const query = threadId === undefined ? '' : `?threadId=${encodeURIComponent(threadId)}`;\n return this.request<SkillCatalogEntry[]>('GET', `/skills${query}`);\n }\n\n /**\n * The tools this caller can reach through `agent` (the default agent when omitted), each with the\n * server-declared `presentation` a chat narrates it by — the same list the model is offered.\n * Prefer {@link useToolCatalog}, which fetches it once and shares it.\n */\n listTools(agent?: string): Promise<ToolCatalogEntry[]> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ToolCatalogEntry[]>('GET', `/tools${query}`);\n }\n\n getThread(id: string): Promise<ThreadDetail> {\n return this.request<ThreadDetail>('GET', `/threads/${encodeURIComponent(id)}`);\n }\n\n deleteThread(id: string): Promise<void> {\n return this.request<void>('DELETE', `/threads/${encodeURIComponent(id)}`);\n }\n\n forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary> {\n return this.request<ThreadSummary>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */\n updateThread(id: string, patch: ThreadPatch): Promise<OkResult> {\n return this.request<OkResult>('PATCH', `/threads/${encodeURIComponent(id)}`, patch);\n }\n\n /**\n * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —\n * mirrors the backend's `POST <path>/attachments`. The returned {@link MessageAttachment} is\n * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.\n */\n async uploadAttachment(\n file: File,\n options: UploadAttachmentOptions = {},\n ): Promise<MessageAttachment> {\n const upload = this.options.attachments?.upload;\n if (upload !== undefined) {\n return upload(file, options, this.connection());\n }\n // `fetch` cannot observe an upload's progress; XHR can. Only when someone is listening, and\n // never when a `fetch` was injected (tests, non-browser runtimes).\n if (\n options.onProgress !== undefined &&\n this.options.fetch === undefined &&\n typeof XMLHttpRequest !== 'undefined'\n ) {\n return this.uploadWithProgress(file, options);\n }\n const formData = new FormData();\n formData.append('file', file);\n const response = await this.fetchImpl()(`${this.root()}/attachments`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n },\n body: formData,\n ...this.credentials(),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n });\n const attachment = await this.handleResponse<MessageAttachment>(\n response,\n 'POST',\n `${this.agentPath()}/attachments`,\n );\n options.onProgress?.(1);\n return attachment;\n }\n\n private async uploadWithProgress(\n file: File,\n { signal, onProgress }: UploadAttachmentOptions,\n ): Promise<MessageAttachment> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n };\n const url = `${this.root()}/attachments`;\n return new Promise<MessageAttachment>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', url);\n for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value);\n // Same-origin requests carry cookies regardless; this is the cross-origin opt-in.\n xhr.withCredentials = this.options.credentials === 'include';\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable && event.total > 0) onProgress?.(event.loaded / event.total);\n };\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n const error = new AgentHttpError(\n xhr.status,\n 'POST',\n `${this.agentPath()}/attachments`,\n xhr.statusText,\n readErrorAnswer(xhr.responseText),\n );\n reportHttpError(this.options.onHttpError, error);\n reject(error);\n return;\n }\n onProgress?.(1);\n resolve(JSON.parse(xhr.responseText) as MessageAttachment);\n };\n xhr.onerror = () => reject(new TypeError('Network error while uploading the attachment'));\n xhr.onabort = () => reject(new DOMException('The upload was aborted', 'AbortError'));\n if (signal !== undefined) {\n if (signal.aborted) {\n reject(new DOMException('The upload was aborted', 'AbortError'));\n return;\n }\n signal.addEventListener('abort', () => xhr.abort(), { once: true });\n }\n const formData = new FormData();\n formData.append('file', file);\n xhr.send(formData);\n });\n }\n\n promoteThread(id: string): Promise<OkResult> {\n return this.request<OkResult>('POST', `/threads/${encodeURIComponent(id)}/promote`);\n }\n\n truncateFromMessage(threadId: string, messageId: string): Promise<OkResult> {\n return this.request<OkResult>(\n 'DELETE',\n `/threads/${encodeURIComponent(threadId)}/from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** `GET <path>/models?agent=` — the models this caller may pick, grouped by provider. */\n listModels(agent?: string): Promise<ModelCatalogView> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ModelCatalogView>('GET', `/models${query}`);\n }\n\n /** `GET <path>/agents` — the registered agents, the default one flagged. */\n listAgents(): Promise<AgentCatalogEntry[]> {\n return this.request<AgentCatalogEntry[]>('GET', '/agents');\n }\n\n /** `GET <path>/config` — attachment limits and upload mode, and which features are on. */\n getConfig(): Promise<AgentClientConfig> {\n return this.request<AgentClientConfig>('GET', '/config');\n }\n\n /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */\n getQuota(): Promise<QuotaReport> {\n return this.request<QuotaReport>('GET', '/quota');\n }\n\n cancelStream(runId: string): Promise<CancelResult> {\n return this.request<CancelResult>('POST', `/chat/${encodeURIComponent(runId)}/cancel`);\n }\n\n /**\n * `remember` approves later calls of the same tool in the same thread; `via` names the surface\n * the decision came through (the server records `'web'` when omitted).\n */\n approveToolCall(input: { toolCallId: string; remember?: boolean; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/approve', input);\n }\n\n rejectToolCall(input: { toolCallId: string; reason?: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/reject', input);\n }\n\n /**\n * Settle a parked question set. `answers` is questionId → chosen option values; a question left\n * out takes the pre-picked default the request carried, resolved server-side against the request\n * the run already holds. Omit the whole object and the user has confirmed every pre-picked\n * answer — which is the point of the surface, so it is a valid submission rather than a blank.\n * `via` names the surface the answer came through (the server records `'web'` when omitted).\n */\n answerToolCall(input: {\n toolCallId: string;\n answers?: Record<string, string[]>;\n via?: string;\n }): Promise<void> {\n return this.request<void>('POST', '/tool-call/answer', input);\n }\n\n /**\n * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same\n * values a confirmation would, and persists differently on purpose — only one of them is\n * evidence the user chose them.\n */\n skipToolCall(input: { toolCallId: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/skip', input);\n }\n\n private fetchImpl(): typeof fetch {\n return this.options.fetch ?? globalThis.fetch;\n }\n\n /** This client's connection, for an {@link AttachmentUploadStrategy}. */\n private connection(): AgentConnection {\n return {\n baseUrl: this.baseUrl(),\n path: this.agentPath(),\n headers: () => this.resolveHeaders(),\n fetch: this.fetchImpl(),\n ...this.credentials(),\n ...(this.options.onHttpError !== undefined ? { onHttpError: this.options.onHttpError } : {}),\n };\n }\n\n private baseUrl(): string {\n return (this.options.baseUrl ?? '').replace(/\\/+$/, '');\n }\n\n private agentPath(): string {\n return normalizeAgentPath(this.options.path);\n }\n\n /** Origin + agent path: what every route hangs off. */\n private root(): string {\n return `${this.baseUrl()}${this.agentPath()}`;\n }\n\n private async resolveHeaders(): Promise<Record<string, string>> {\n const dynamic = (await this.options.getHeaders?.()) ?? {};\n return { ...this.options.headers, ...dynamic };\n }\n\n private credentials(): { credentials?: RequestCredentials } {\n return this.options.credentials !== undefined ? { credentials: this.options.credentials } : {};\n }\n\n private async request<T>(method: string, route: string, body?: unknown): Promise<T> {\n const path = `${this.agentPath()}${route}`;\n const response = await this.fetchImpl()(`${this.baseUrl()}${path}`, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await this.resolveHeaders()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n ...this.credentials(),\n });\n return this.handleResponse<T>(response, method, path);\n }\n\n /** The error for a refused `response`, already reported to `onHttpError`. */\n private async failure(response: Response, method: string, path: string): Promise<AgentHttpError> {\n const error = await AgentHttpError.from(response, method, path);\n reportHttpError(this.options.onHttpError, error);\n return error;\n }\n\n private async handleResponse<T>(response: Response, method: string, path: string): Promise<T> {\n if (!response.ok) {\n throw await this.failure(response, method, path);\n }\n if (response.status === 204) return undefined as T;\n const text = await response.text();\n if (!text) return undefined as T;\n return JSON.parse(text) as T;\n }\n}\n\nfunction emptyStream(): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n}\n\nfunction streamResponse(response: Response): ChatStreamResponse {\n const body = response.body as ReadableStream<Uint8Array>;\n const runId = response.headers?.get(HEADER_RUN_ID) ?? undefined;\n const threadId = response.headers?.get(HEADER_THREAD_ID) ?? undefined;\n return {\n body,\n ...(runId ? { runId } : {}),\n ...(threadId ? { threadId } : {}),\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/media/index.ts","../src/http-error.ts","../src/client.ts"],"sourcesContent":["import type { MessageAttachment } from '@dudousxd/nestjs-agent-core';\nimport { streamChunks } from '@dudousxd/nestjs-media-client';\nimport type { AttachmentUploadStrategy, UploadAttachmentOptions } from '../backend.js';\nimport { type AgentClientOptions, normalizeAgentPath } from '../client.js';\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from '../http-error.js';\n\n/** Tuning for {@link mediaAttachments}; every field optional. */\nexport interface MediaAttachmentsOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB. */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/**\n * Resumable attachment uploads through `@dudousxd/nestjs-media`, in one line:\n *\n * ```tsx\n * <AgentProvider attachments={{ upload: mediaAttachments() }}>\n * ```\n *\n * Reuses the client's own connection (origin, path, headers, credentials), so it needs no\n * configuration. Also takes `new AgentClient({ attachments: { upload: mediaAttachments() } })`.\n */\nexport function mediaAttachments(options: MediaAttachmentsOptions = {}): AttachmentUploadStrategy {\n return (file, uploadOptions, connection) =>\n createMediaUpload({\n ...options,\n baseUrl: connection.baseUrl,\n path: connection.path,\n getHeaders: connection.headers,\n fetch: connection.fetch,\n ...(connection.credentials !== undefined ? { credentials: connection.credentials } : {}),\n ...(connection.onHttpError !== undefined ? { onHttpError: connection.onHttpError } : {}),\n })(file, uploadOptions);\n}\n\n/**\n * Where and how {@link createMediaUpload} talks to the server. The connection fields are\n * {@link AgentClientOptions}' — pass the same ones your `AgentClient` gets, so the tus requests\n * carry the same cookies / CSRF header / bearer token as the rest of the chat.\n */\nexport interface MediaUploadOptions extends AgentClientOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB (nestjs-media-client's default). */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/** What `useAttachments({ upload })` and `AgentBackend.uploadAttachment` both take. */\nexport type AttachmentUpload = (\n file: File,\n options?: UploadAttachmentOptions,\n) => Promise<MessageAttachment>;\n\n/**\n * A non-2xx answer from the agent's upload routes. Carries what `AgentHttpError` does — `status`\n * (413 too large, 415 type refused, 404 not yours), the server's `message`, `code` and `body` —\n * as its own class because this subpath is bundled apart from the root entry, where an\n * `instanceof AgentHttpError` would not match a copy. Both satisfy `AgentRequestError`.\n */\nexport class MediaUploadError extends Error implements AgentRequestError {\n readonly body: unknown;\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(\n answer.message ?? `Attachment upload failed: ${method} ${path} → ${status} ${statusText}`,\n );\n this.name = 'MediaUploadError';\n this.body = answer.body;\n this.code = answer.code;\n }\n}\n\ninterface BeginResponse {\n mediaId: string;\n location: string;\n}\n\n/**\n * A resumable `upload` for `useAttachments`, backed by `@dudousxd/nestjs-media` on the server\n * (`AgentMediaAttachmentsModule` from `@dudousxd/nestjs-agent/media`):\n *\n * 1. `POST <path>/attachments/uploads` — the agent validates the file and opens a tus session the\n * actor owns;\n * 2. the bytes stream to nestjs-media's own tus endpoint in chunks (`streamChunks`), reporting\n * progress and honouring the abort signal;\n * 3. `POST <path>/attachments/uploads/:mediaId/complete` — the agent confirms the bytes landed and\n * answers the `MessageAttachment` the turn will reference by `mediaId`.\n *\n * An aborted or failed upload is discarded server-side (`DELETE`), best effort.\n */\nexport function createMediaUpload(options: MediaUploadOptions = {}): AttachmentUpload {\n const origin = (options.baseUrl ?? '').replace(/\\/$/, '');\n const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;\n const baseFetch = (): typeof fetch => options.fetch ?? fetch;\n // Every request — the agent's and media's tus PATCHes — rides the same credentials mode.\n const fetchWithCredentials: typeof fetch = (input, init) =>\n baseFetch()(input, {\n ...init,\n ...(options.credentials !== undefined ? { credentials: options.credentials } : {}),\n });\n const headers = async (): Promise<Record<string, string>> => ({\n ...options.headers,\n ...((await options.getHeaders?.()) ?? {}),\n });\n const discard = (mediaId: string): void => {\n void (async () => {\n await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {\n method: 'DELETE',\n headers: await headers(),\n });\n })().catch(() => undefined);\n };\n const call = async <T>(method: string, url: string, body?: unknown): Promise<T> => {\n const response = await fetchWithCredentials(url, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await headers()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n if (!response.ok) {\n const error = new MediaUploadError(\n response.status,\n method,\n url,\n response.statusText,\n await readErrorResponse(response),\n );\n reportHttpError(options.onHttpError, error);\n throw error;\n }\n return (await response.json()) as T;\n };\n\n return async (file, { signal, onProgress } = {}) => {\n signal?.throwIfAborted();\n const begun = await call<BeginResponse>('POST', base, {\n filename: file.name,\n contentType: file.type,\n size: file.size,\n });\n const onAbort = () => discard(begun.mediaId);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n const location = /^https?:\\/\\//.test(begun.location)\n ? begun.location\n : `${origin}${begun.location}`;\n await streamChunks(location, file, {\n resume: false,\n fetchImpl: fetchWithCredentials,\n getHeaders: headers,\n ...(options.chunkSize !== undefined ? { chunkSize: options.chunkSize } : {}),\n ...(options.retries !== undefined ? { retries: options.retries } : {}),\n ...(signal !== undefined ? { signal } : {}),\n onProgress: (sent, total) => onProgress?.(total > 0 ? sent / total : 1),\n });\n signal?.throwIfAborted();\n const attachment = await call<MessageAttachment>(\n 'POST',\n `${base}/${encodeURIComponent(begun.mediaId)}/complete`,\n );\n onProgress?.(1);\n return attachment;\n } catch (error) {\n if (!signal?.aborted) discard(begun.mediaId);\n throw error;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n","/**\n * What a server said when it refused a request, read once so every error class of this package\n * reports it the same way. Bundled into each entry that throws (the root and `/media`), which is\n * why it holds no class of its own.\n */\nexport interface ErrorAnswer {\n /** The body, parsed as JSON when it is JSON, else the raw text; `undefined` when empty. */\n body: unknown;\n /** `body.message` — a string, or NestJS's list of validation messages joined with `; `. */\n message: string | undefined;\n /** `body.code`, the machine-readable reason (`quota_exceeded`, …), when the server sent one. */\n code: string | undefined;\n}\n\n/** Read an error answer's body text. Never throws: a body that cannot be read is just absent. */\nexport function readErrorAnswer(text: string | null | undefined): ErrorAnswer {\n if (text == null || text === '') return { body: undefined, message: undefined, code: undefined };\n let body: unknown = text;\n try {\n body = JSON.parse(text);\n } catch {\n return { body: text, message: undefined, code: undefined };\n }\n if (body === null || typeof body !== 'object')\n return { body, message: undefined, code: undefined };\n const record = body as { message?: unknown; code?: unknown };\n const message =\n typeof record.message === 'string' && record.message !== ''\n ? record.message\n : Array.isArray(record.message) &&\n record.message.length > 0 &&\n record.message.every((each) => typeof each === 'string')\n ? record.message.join('; ')\n : undefined;\n const code = typeof record.code === 'string' && record.code !== '' ? record.code : undefined;\n return { body, message, code };\n}\n\n/** {@link readErrorAnswer} over a `Response` whose status is not 2xx. */\nexport async function readErrorResponse(response: Response): Promise<ErrorAnswer> {\n let text: string | undefined;\n try {\n text = await response.text();\n } catch {\n text = undefined;\n }\n return readErrorAnswer(text);\n}\n\n/**\n * The shape both {@link import('./client.js').AgentHttpError} and `MediaUploadError` share, for\n * code that handles either without an `instanceof` (the two live in separate bundles).\n */\nexport interface AgentRequestError extends Error {\n status: number;\n method: string;\n path: string;\n body: unknown;\n code: string | undefined;\n}\n\n/** Called with every error answer before it is thrown — for app-wide reactions (401, 402, …). */\nexport type HttpErrorListener = (error: AgentRequestError) => void;\n\n/** Tell `listener` about `error`; a listener that throws never replaces the error itself. */\nexport function reportHttpError(\n listener: HttpErrorListener | undefined,\n error: AgentRequestError,\n): void {\n if (listener === undefined) return;\n try {\n listener(error);\n } catch {\n /* the request's own error is what the caller gets */\n }\n}\n","import type {\n ActionProposalMutationView,\n ActionProposalView,\n AgentCatalogEntry,\n AgentClientConfig,\n ChatQueueState,\n MessageAttachment,\n MessageFeedback,\n ModelCatalogView,\n QuotaReport,\n SkillCatalogEntry,\n ThreadDetail,\n ThreadSummary,\n ToolCatalogEntry,\n} from '@dudousxd/nestjs-agent-core';\nimport type {\n AgentBackend,\n AgentConnection,\n AttachmentUploadStrategy,\n ChatStreamRequest,\n ChatStreamResponse,\n MessageFeedbackInput,\n QueuedMessageUpdate,\n QueuedSendResult,\n ResumeStreamRequest,\n ThreadPatch,\n UploadAttachmentOptions,\n} from './backend.js';\n\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n type HttpErrorListener,\n readErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from './http-error.js';\n\nexport type { ThreadPatch } from './backend.js';\nexport type { AgentRequestError, HttpErrorListener } from './http-error.js';\n\n/**\n * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can\n * branch (e.g. 403 → \"not your thread\", 429 → quota) instead of string-matching a generic Error,\n * and what the server said: `message` is the body's `message` when it sent one (so a hook that\n * shows `error.message` shows the server's words), `code` its machine-readable `code`, and `body`\n * the whole answer, parsed when it is JSON.\n */\nexport class AgentHttpError extends Error implements AgentRequestError {\n /** The answer's body: parsed JSON, else its text; `undefined` when empty. */\n readonly body: unknown;\n /** The body's `code` (`quota_exceeded`, …), when the server sent one. */\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(answer.message ?? `Agent request failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'AgentHttpError';\n this.body = answer.body;\n this.code = answer.code;\n }\n\n /** Read a refused `response`'s body into an error. */\n static async from(response: Response, method: string, path: string): Promise<AgentHttpError> {\n return new AgentHttpError(\n response.status,\n method,\n path,\n response.statusText,\n await readErrorResponse(response),\n );\n }\n}\n\nexport interface CancelResult {\n aborted: boolean;\n}\n\nexport interface OkResult {\n ok: boolean;\n}\n\nexport interface AgentClientOptions {\n /**\n * The server's origin, e.g. `https://api.example.com`. Defaults to `''` (same origin). The\n * agent's route prefix is {@link AgentClientOptions.path}, not part of this.\n */\n baseUrl?: string;\n /**\n * The agent's route prefix — `AgentModule`'s `path`, with any global prefix in front\n * (`'api/agent'`). Leading/trailing slashes are optional. Defaults to `'agent'`.\n */\n path?: string;\n /** Static headers merged into every request. */\n headers?: Record<string, string>;\n /**\n * Resolved per request — for short-lived bearer tokens, or a CSRF header read from a cookie\n * (`{ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }`), which has to be read at request time because\n * the server may rotate it.\n */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /**\n * Forwarded to fetch so cookie auth works. Same-origin requests send cookies by default; set\n * `'include'` when the API lives on another origin (and have it answer with credentialed CORS).\n */\n credentials?: RequestCredentials;\n /** Injectable for tests / non-browser runtimes. */\n fetch?: typeof fetch;\n /**\n * Called with every error answer (an {@link AgentHttpError}, or a `MediaUploadError` from a\n * `mediaAttachments()` upload) right before it is thrown — the place for app-wide reactions such\n * as \"401 → sign in again\". The error still reaches the caller. Not called for the `404` a\n * resume answers when nothing is streaming, which is an answer, not a failure.\n */\n onHttpError?: HttpErrorListener;\n /** Attachment uploads. */\n attachments?: {\n /**\n * How `uploadAttachment` uploads. Omitted → `POST <path>/attachments` (multipart). Pass\n * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through\n * nestjs-media, or your own {@link AttachmentUploadStrategy}.\n */\n upload?: AttachmentUploadStrategy;\n };\n}\n\n/** `'/api/agent'` from `'api/agent'`, `'/api/agent/'`, …; `''` for an empty path. */\nexport function normalizeAgentPath(path: string | undefined): string {\n const trimmed = (path ?? 'agent').replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `/${trimmed}`;\n}\n\nconst HEADER_RUN_ID = 'x-agent-run-id';\nconst HEADER_THREAD_ID = 'x-agent-thread-id';\n\n/**\n * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.\n * Used by `useAgentChat`, but standalone-usable (vanilla fetch, no React).\n */\nexport class AgentClient implements AgentBackend {\n constructor(private readonly options: AgentClientOptions = {}) {}\n\n /** `POST <path>/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */\n async openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse> {\n const response = await this.fetchImpl()(`${this.root()}/chat`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n body: JSON.stringify(request.body),\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 202) {\n // Queued behind a turn already running on the thread: JSON, not a stream.\n return { body: emptyStream(), queued: (await response.json()) as QueuedSendResult };\n }\n if (response.ok && response.headers.get('content-type')?.includes('application/json')) {\n const decision = (await response.json()) as NonNullable<\n ChatStreamResponse['proposalDecision']\n >;\n if (decision.proposalDecision !== undefined && typeof decision.threadId === 'string')\n return { body: emptyStream(), threadId: decision.threadId, proposalDecision: decision };\n throw new Error('Unexpected JSON chat response');\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'POST', `${this.agentPath()}/chat`);\n }\n return streamResponse(response);\n }\n\n /**\n * `POST <path>/chat` for a message that should wait in the thread's queue — the body carries\n * `mode: 'queue'` (or `'interrupt'`), which the server always answers with `202` JSON.\n */\n enqueueMessage(request: ChatStreamRequest): Promise<QueuedSendResult> {\n return this.request<QueuedSendResult>('POST', '/chat', {\n mode: 'queue',\n ...request.body,\n });\n }\n\n /** `GET <path>/threads/:id/queue` — the thread's waiting messages, and whether it drains. */\n getQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('GET', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `PATCH <path>/queue/:messageId` — change a waiting message's text/attachments, or move it. */\n updateQueuedMessage(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('PATCH', `/queue/${encodeURIComponent(messageId)}`, update);\n }\n\n /** `DELETE <path>/queue/:messageId`. */\n removeQueuedMessage(messageId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/queue/${encodeURIComponent(messageId)}`);\n }\n\n /**\n * `POST <path>/queue/:messageId/interrupt` — run a waiting message now, cancelling the running\n * turn for it. `runId` when nothing was running and it started; else `interrupting`.\n */\n interruptQueuedMessage(\n messageId: string,\n ): Promise<ChatQueueState & { runId?: string; interrupting?: string }> {\n return this.request<ChatQueueState & { runId?: string; interrupting?: string }>(\n 'POST',\n `/queue/${encodeURIComponent(messageId)}/interrupt`,\n );\n }\n\n /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */\n clearQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */\n resumeQueue(threadId: string): Promise<ChatQueueState & { runId?: string }> {\n return this.request<ChatQueueState & { runId?: string }>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/queue/resume`,\n );\n }\n\n /**\n * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is\n * streaming under that id (404).\n */\n async resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null> {\n const path = `/chat/${encodeURIComponent(request.runId)}/stream`;\n const query = request.after !== undefined && request.after > 0 ? `?after=${request.after}` : '';\n const response = await this.fetchImpl()(`${this.root()}${path}${query}`, {\n method: 'GET',\n headers: {\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 404) {\n return null;\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'GET', `${this.agentPath()}${path}`);\n }\n return streamResponse(response);\n }\n\n /**\n * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers\n * the stored rating.\n */\n setMessageFeedback(\n messageId: string,\n input: MessageFeedbackInput,\n ): Promise<{ feedback: MessageFeedback | null }> {\n return this.request<{ feedback: MessageFeedback | null }>(\n 'POST',\n `/messages/${encodeURIComponent(messageId)}/feedback`,\n input,\n );\n }\n\n listThreads(): Promise<ThreadSummary[]> {\n return this.request<ThreadSummary[]>('GET', '/threads');\n }\n\n /**\n * The skills this caller can invoke right now, scope-resolved — the same list, built by the same\n * call, that the model is offered, so what a user can type after a `/` and what the agent can\n * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to\n * one conversation; omitted, the server reads it as a brand-new thread.\n */\n listSkills(threadId?: string): Promise<SkillCatalogEntry[]> {\n const query = threadId === undefined ? '' : `?threadId=${encodeURIComponent(threadId)}`;\n return this.request<SkillCatalogEntry[]>('GET', `/skills${query}`);\n }\n\n /**\n * The tools this caller can reach through `agent` (the default agent when omitted), each with the\n * server-declared `presentation` a chat narrates it by — the same list the model is offered.\n * Prefer {@link useToolCatalog}, which fetches it once and shares it.\n */\n listTools(agent?: string): Promise<ToolCatalogEntry[]> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ToolCatalogEntry[]>('GET', `/tools${query}`);\n }\n\n getThread(id: string): Promise<ThreadDetail> {\n return this.request<ThreadDetail>('GET', `/threads/${encodeURIComponent(id)}`);\n }\n\n deleteThread(id: string): Promise<void> {\n return this.request<void>('DELETE', `/threads/${encodeURIComponent(id)}`);\n }\n\n forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary> {\n return this.request<ThreadSummary>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */\n updateThread(id: string, patch: ThreadPatch): Promise<OkResult> {\n return this.request<OkResult>('PATCH', `/threads/${encodeURIComponent(id)}`, patch);\n }\n\n /**\n * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —\n * mirrors the backend's `POST <path>/attachments`. The returned {@link MessageAttachment} is\n * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.\n */\n async uploadAttachment(\n file: File,\n options: UploadAttachmentOptions = {},\n ): Promise<MessageAttachment> {\n const upload = this.options.attachments?.upload;\n if (upload !== undefined) {\n return upload(file, options, this.connection());\n }\n // `fetch` cannot observe an upload's progress; XHR can. Only when someone is listening, and\n // never when a `fetch` was injected (tests, non-browser runtimes).\n if (\n options.onProgress !== undefined &&\n this.options.fetch === undefined &&\n typeof XMLHttpRequest !== 'undefined'\n ) {\n return this.uploadWithProgress(file, options);\n }\n const formData = new FormData();\n formData.append('file', file);\n const response = await this.fetchImpl()(`${this.root()}/attachments`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n },\n body: formData,\n ...this.credentials(),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n });\n const attachment = await this.handleResponse<MessageAttachment>(\n response,\n 'POST',\n `${this.agentPath()}/attachments`,\n );\n options.onProgress?.(1);\n return attachment;\n }\n\n private async uploadWithProgress(\n file: File,\n { signal, onProgress }: UploadAttachmentOptions,\n ): Promise<MessageAttachment> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n };\n const url = `${this.root()}/attachments`;\n return new Promise<MessageAttachment>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', url);\n for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value);\n // Same-origin requests carry cookies regardless; this is the cross-origin opt-in.\n xhr.withCredentials = this.options.credentials === 'include';\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable && event.total > 0) onProgress?.(event.loaded / event.total);\n };\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n const error = new AgentHttpError(\n xhr.status,\n 'POST',\n `${this.agentPath()}/attachments`,\n xhr.statusText,\n readErrorAnswer(xhr.responseText),\n );\n reportHttpError(this.options.onHttpError, error);\n reject(error);\n return;\n }\n onProgress?.(1);\n resolve(JSON.parse(xhr.responseText) as MessageAttachment);\n };\n xhr.onerror = () => reject(new TypeError('Network error while uploading the attachment'));\n xhr.onabort = () => reject(new DOMException('The upload was aborted', 'AbortError'));\n if (signal !== undefined) {\n if (signal.aborted) {\n reject(new DOMException('The upload was aborted', 'AbortError'));\n return;\n }\n signal.addEventListener('abort', () => xhr.abort(), { once: true });\n }\n const formData = new FormData();\n formData.append('file', file);\n xhr.send(formData);\n });\n }\n\n promoteThread(id: string): Promise<OkResult> {\n return this.request<OkResult>('POST', `/threads/${encodeURIComponent(id)}/promote`);\n }\n\n truncateFromMessage(threadId: string, messageId: string): Promise<OkResult> {\n return this.request<OkResult>(\n 'DELETE',\n `/threads/${encodeURIComponent(threadId)}/from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** `GET <path>/models?agent=` — the models this caller may pick, grouped by provider. */\n listModels(agent?: string): Promise<ModelCatalogView> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ModelCatalogView>('GET', `/models${query}`);\n }\n\n /** `GET <path>/agents` — the registered agents, the default one flagged. */\n listAgents(): Promise<AgentCatalogEntry[]> {\n return this.request<AgentCatalogEntry[]>('GET', '/agents');\n }\n\n /** `GET <path>/config` — attachment limits and upload mode, and which features are on. */\n getConfig(): Promise<AgentClientConfig> {\n return this.request<AgentClientConfig>('GET', '/config');\n }\n\n /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */\n getQuota(): Promise<QuotaReport> {\n return this.request<QuotaReport>('GET', '/quota');\n }\n\n cancelStream(runId: string): Promise<CancelResult> {\n return this.request<CancelResult>('POST', `/chat/${encodeURIComponent(runId)}/cancel`);\n }\n\n /**\n * `remember` approves later calls of the same tool in the same thread; `via` names the surface\n * the decision came through (the server records `'web'` when omitted).\n */\n async listActionProposals({ threadId }: { threadId: string }): Promise<ActionProposalView[]> {\n const proposals = new Map<string, ActionProposalView>();\n let after: { createdAt: number; id: string } | undefined;\n for (;;) {\n const route = `/threads/${encodeURIComponent(threadId)}/action-proposals${\n after === undefined ? '' : `?after=${encodeURIComponent(JSON.stringify(after))}`\n }`;\n const response = await this.requestResponse('GET', route);\n const page = await this.handleResponse<unknown>(\n response,\n 'GET',\n `${this.agentPath()}${route}`,\n );\n if (!Array.isArray(page)) throw new TypeError('Invalid action proposal page');\n for (const proposal of page) proposals.set(proposal.id, proposal);\n const header = response.headers.get('X-Action-Proposals-Next');\n if (header === null) return [...proposals.values()];\n let next: unknown;\n try {\n next = JSON.parse(decodeURIComponent(header));\n } catch {\n throw new TypeError('Invalid action proposal cursor');\n }\n if (\n typeof next !== 'object' ||\n next === null ||\n !('createdAt' in next) ||\n typeof next.createdAt !== 'number' ||\n !Number.isSafeInteger(next.createdAt) ||\n !('id' in next) ||\n typeof next.id !== 'string' ||\n next.id.length === 0 ||\n next.id.length > 255 ||\n (after !== undefined &&\n (next.createdAt < after.createdAt ||\n (next.createdAt === after.createdAt && next.id <= after.id)))\n )\n throw new TypeError('Invalid or non-advancing action proposal cursor');\n after = { createdAt: next.createdAt, id: next.id };\n }\n }\n approveActionProposal({\n threadId,\n proposalId,\n remember,\n }: {\n threadId: string;\n proposalId: string;\n remember?: boolean;\n }): Promise<ActionProposalMutationView> {\n return this.request(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/action-proposals/${encodeURIComponent(proposalId)}/approve`,\n remember === undefined ? {} : { remember },\n );\n }\n rejectActionProposal({\n threadId,\n proposalId,\n reason,\n }: {\n threadId: string;\n proposalId: string;\n reason?: string;\n }): Promise<ActionProposalMutationView> {\n return this.request(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/action-proposals/${encodeURIComponent(proposalId)}/reject`,\n reason === undefined ? {} : { reason },\n );\n }\n\n approveToolCall(input: { toolCallId: string; remember?: boolean; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/approve', input);\n }\n\n rejectToolCall(input: { toolCallId: string; reason?: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/reject', input);\n }\n\n /**\n * Settle a parked question set. `answers` is questionId → chosen option values; a question left\n * out takes the pre-picked default the request carried, resolved server-side against the request\n * the run already holds. Omit the whole object and the user has confirmed every pre-picked\n * answer — which is the point of the surface, so it is a valid submission rather than a blank.\n * `via` names the surface the answer came through (the server records `'web'` when omitted).\n */\n answerToolCall(input: {\n toolCallId: string;\n answers?: Record<string, string[]>;\n via?: string;\n }): Promise<void> {\n return this.request<void>('POST', '/tool-call/answer', input);\n }\n\n /**\n * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same\n * values a confirmation would, and persists differently on purpose — only one of them is\n * evidence the user chose them.\n */\n skipToolCall(input: { toolCallId: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/skip', input);\n }\n\n private fetchImpl(): typeof fetch {\n return this.options.fetch ?? globalThis.fetch;\n }\n\n /** This client's connection, for an {@link AttachmentUploadStrategy}. */\n private connection(): AgentConnection {\n return {\n baseUrl: this.baseUrl(),\n path: this.agentPath(),\n headers: () => this.resolveHeaders(),\n fetch: this.fetchImpl(),\n ...this.credentials(),\n ...(this.options.onHttpError !== undefined ? { onHttpError: this.options.onHttpError } : {}),\n };\n }\n\n private baseUrl(): string {\n return (this.options.baseUrl ?? '').replace(/\\/+$/, '');\n }\n\n private agentPath(): string {\n return normalizeAgentPath(this.options.path);\n }\n\n /** Origin + agent path: what every route hangs off. */\n private root(): string {\n return `${this.baseUrl()}${this.agentPath()}`;\n }\n\n private async resolveHeaders(): Promise<Record<string, string>> {\n const dynamic = (await this.options.getHeaders?.()) ?? {};\n return { ...this.options.headers, ...dynamic };\n }\n\n private credentials(): { credentials?: RequestCredentials } {\n return this.options.credentials !== undefined ? { credentials: this.options.credentials } : {};\n }\n\n private async request<T>(method: string, route: string, body?: unknown): Promise<T> {\n const response = await this.requestResponse(method, route, body);\n return this.handleResponse<T>(response, method, `${this.agentPath()}${route}`);\n }\n\n private async requestResponse(method: string, route: string, body?: unknown): Promise<Response> {\n const path = `${this.agentPath()}${route}`;\n const response = await this.fetchImpl()(`${this.baseUrl()}${path}`, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await this.resolveHeaders()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n ...this.credentials(),\n });\n return response;\n }\n\n /** The error for a refused `response`, already reported to `onHttpError`. */\n private async failure(response: Response, method: string, path: string): Promise<AgentHttpError> {\n const error = await AgentHttpError.from(response, method, path);\n reportHttpError(this.options.onHttpError, error);\n return error;\n }\n\n private async handleResponse<T>(response: Response, method: string, path: string): Promise<T> {\n if (!response.ok) {\n throw await this.failure(response, method, path);\n }\n if (response.status === 204) return undefined as T;\n const text = await response.text();\n if (!text) return undefined as T;\n return JSON.parse(text) as T;\n }\n}\n\nfunction emptyStream(): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n}\n\nfunction streamResponse(response: Response): ChatStreamResponse {\n const body = response.body as ReadableStream<Uint8Array>;\n const runId = response.headers?.get(HEADER_RUN_ID) ?? undefined;\n const threadId = response.headers?.get(HEADER_THREAD_ID) ?? undefined;\n return {\n body,\n ...(runId ? { runId } : {}),\n ...(threadId ? { threadId } : {}),\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/media.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MessageAttachment } from '@dudousxd/nestjs-agent-core';
|
|
2
|
-
import { U as UploadAttachmentOptions, A as AgentRequestError, E as ErrorAnswer, f as AgentClientOptions, c as AttachmentUploadStrategy } from './client-
|
|
2
|
+
import { U as UploadAttachmentOptions, A as AgentRequestError, E as ErrorAnswer, f as AgentClientOptions, c as AttachmentUploadStrategy } from './client-BdHUuqy-.cjs';
|
|
3
3
|
|
|
4
4
|
/** Tuning for {@link mediaAttachments}; every field optional. */
|
|
5
5
|
interface MediaAttachmentsOptions {
|
package/dist/media.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MessageAttachment } from '@dudousxd/nestjs-agent-core';
|
|
2
|
-
import { U as UploadAttachmentOptions, A as AgentRequestError, E as ErrorAnswer, f as AgentClientOptions, c as AttachmentUploadStrategy } from './client-
|
|
2
|
+
import { U as UploadAttachmentOptions, A as AgentRequestError, E as ErrorAnswer, f as AgentClientOptions, c as AttachmentUploadStrategy } from './client-BdHUuqy-.js';
|
|
3
3
|
|
|
4
4
|
/** Tuning for {@link mediaAttachments}; every field optional. */
|
|
5
5
|
interface MediaAttachmentsOptions {
|
package/dist/media.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/media/index.ts","../src/http-error.ts","../src/client.ts"],"sourcesContent":["import type { MessageAttachment } from '@dudousxd/nestjs-agent-core';\nimport { streamChunks } from '@dudousxd/nestjs-media-client';\nimport type { AttachmentUploadStrategy, UploadAttachmentOptions } from '../backend.js';\nimport { type AgentClientOptions, normalizeAgentPath } from '../client.js';\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from '../http-error.js';\n\n/** Tuning for {@link mediaAttachments}; every field optional. */\nexport interface MediaAttachmentsOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB. */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/**\n * Resumable attachment uploads through `@dudousxd/nestjs-media`, in one line:\n *\n * ```tsx\n * <AgentProvider attachments={{ upload: mediaAttachments() }}>\n * ```\n *\n * Reuses the client's own connection (origin, path, headers, credentials), so it needs no\n * configuration. Also takes `new AgentClient({ attachments: { upload: mediaAttachments() } })`.\n */\nexport function mediaAttachments(options: MediaAttachmentsOptions = {}): AttachmentUploadStrategy {\n return (file, uploadOptions, connection) =>\n createMediaUpload({\n ...options,\n baseUrl: connection.baseUrl,\n path: connection.path,\n getHeaders: connection.headers,\n fetch: connection.fetch,\n ...(connection.credentials !== undefined ? { credentials: connection.credentials } : {}),\n ...(connection.onHttpError !== undefined ? { onHttpError: connection.onHttpError } : {}),\n })(file, uploadOptions);\n}\n\n/**\n * Where and how {@link createMediaUpload} talks to the server. The connection fields are\n * {@link AgentClientOptions}' — pass the same ones your `AgentClient` gets, so the tus requests\n * carry the same cookies / CSRF header / bearer token as the rest of the chat.\n */\nexport interface MediaUploadOptions extends AgentClientOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB (nestjs-media-client's default). */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/** What `useAttachments({ upload })` and `AgentBackend.uploadAttachment` both take. */\nexport type AttachmentUpload = (\n file: File,\n options?: UploadAttachmentOptions,\n) => Promise<MessageAttachment>;\n\n/**\n * A non-2xx answer from the agent's upload routes. Carries what `AgentHttpError` does — `status`\n * (413 too large, 415 type refused, 404 not yours), the server's `message`, `code` and `body` —\n * as its own class because this subpath is bundled apart from the root entry, where an\n * `instanceof AgentHttpError` would not match a copy. Both satisfy `AgentRequestError`.\n */\nexport class MediaUploadError extends Error implements AgentRequestError {\n readonly body: unknown;\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(\n answer.message ?? `Attachment upload failed: ${method} ${path} → ${status} ${statusText}`,\n );\n this.name = 'MediaUploadError';\n this.body = answer.body;\n this.code = answer.code;\n }\n}\n\ninterface BeginResponse {\n mediaId: string;\n location: string;\n}\n\n/**\n * A resumable `upload` for `useAttachments`, backed by `@dudousxd/nestjs-media` on the server\n * (`AgentMediaAttachmentsModule` from `@dudousxd/nestjs-agent/media`):\n *\n * 1. `POST <path>/attachments/uploads` — the agent validates the file and opens a tus session the\n * actor owns;\n * 2. the bytes stream to nestjs-media's own tus endpoint in chunks (`streamChunks`), reporting\n * progress and honouring the abort signal;\n * 3. `POST <path>/attachments/uploads/:mediaId/complete` — the agent confirms the bytes landed and\n * answers the `MessageAttachment` the turn will reference by `mediaId`.\n *\n * An aborted or failed upload is discarded server-side (`DELETE`), best effort.\n */\nexport function createMediaUpload(options: MediaUploadOptions = {}): AttachmentUpload {\n const origin = (options.baseUrl ?? '').replace(/\\/$/, '');\n const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;\n const baseFetch = (): typeof fetch => options.fetch ?? fetch;\n // Every request — the agent's and media's tus PATCHes — rides the same credentials mode.\n const fetchWithCredentials: typeof fetch = (input, init) =>\n baseFetch()(input, {\n ...init,\n ...(options.credentials !== undefined ? { credentials: options.credentials } : {}),\n });\n const headers = async (): Promise<Record<string, string>> => ({\n ...options.headers,\n ...((await options.getHeaders?.()) ?? {}),\n });\n const discard = (mediaId: string): void => {\n void (async () => {\n await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {\n method: 'DELETE',\n headers: await headers(),\n });\n })().catch(() => undefined);\n };\n const call = async <T>(method: string, url: string, body?: unknown): Promise<T> => {\n const response = await fetchWithCredentials(url, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await headers()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n if (!response.ok) {\n const error = new MediaUploadError(\n response.status,\n method,\n url,\n response.statusText,\n await readErrorResponse(response),\n );\n reportHttpError(options.onHttpError, error);\n throw error;\n }\n return (await response.json()) as T;\n };\n\n return async (file, { signal, onProgress } = {}) => {\n signal?.throwIfAborted();\n const begun = await call<BeginResponse>('POST', base, {\n filename: file.name,\n contentType: file.type,\n size: file.size,\n });\n const onAbort = () => discard(begun.mediaId);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n const location = /^https?:\\/\\//.test(begun.location)\n ? begun.location\n : `${origin}${begun.location}`;\n await streamChunks(location, file, {\n resume: false,\n fetchImpl: fetchWithCredentials,\n getHeaders: headers,\n ...(options.chunkSize !== undefined ? { chunkSize: options.chunkSize } : {}),\n ...(options.retries !== undefined ? { retries: options.retries } : {}),\n ...(signal !== undefined ? { signal } : {}),\n onProgress: (sent, total) => onProgress?.(total > 0 ? sent / total : 1),\n });\n signal?.throwIfAborted();\n const attachment = await call<MessageAttachment>(\n 'POST',\n `${base}/${encodeURIComponent(begun.mediaId)}/complete`,\n );\n onProgress?.(1);\n return attachment;\n } catch (error) {\n if (!signal?.aborted) discard(begun.mediaId);\n throw error;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n","/**\n * What a server said when it refused a request, read once so every error class of this package\n * reports it the same way. Bundled into each entry that throws (the root and `/media`), which is\n * why it holds no class of its own.\n */\nexport interface ErrorAnswer {\n /** The body, parsed as JSON when it is JSON, else the raw text; `undefined` when empty. */\n body: unknown;\n /** `body.message` — a string, or NestJS's list of validation messages joined with `; `. */\n message: string | undefined;\n /** `body.code`, the machine-readable reason (`quota_exceeded`, …), when the server sent one. */\n code: string | undefined;\n}\n\n/** Read an error answer's body text. Never throws: a body that cannot be read is just absent. */\nexport function readErrorAnswer(text: string | null | undefined): ErrorAnswer {\n if (text == null || text === '') return { body: undefined, message: undefined, code: undefined };\n let body: unknown = text;\n try {\n body = JSON.parse(text);\n } catch {\n return { body: text, message: undefined, code: undefined };\n }\n if (body === null || typeof body !== 'object')\n return { body, message: undefined, code: undefined };\n const record = body as { message?: unknown; code?: unknown };\n const message =\n typeof record.message === 'string' && record.message !== ''\n ? record.message\n : Array.isArray(record.message) &&\n record.message.length > 0 &&\n record.message.every((each) => typeof each === 'string')\n ? record.message.join('; ')\n : undefined;\n const code = typeof record.code === 'string' && record.code !== '' ? record.code : undefined;\n return { body, message, code };\n}\n\n/** {@link readErrorAnswer} over a `Response` whose status is not 2xx. */\nexport async function readErrorResponse(response: Response): Promise<ErrorAnswer> {\n let text: string | undefined;\n try {\n text = await response.text();\n } catch {\n text = undefined;\n }\n return readErrorAnswer(text);\n}\n\n/**\n * The shape both {@link import('./client.js').AgentHttpError} and `MediaUploadError` share, for\n * code that handles either without an `instanceof` (the two live in separate bundles).\n */\nexport interface AgentRequestError extends Error {\n status: number;\n method: string;\n path: string;\n body: unknown;\n code: string | undefined;\n}\n\n/** Called with every error answer before it is thrown — for app-wide reactions (401, 402, …). */\nexport type HttpErrorListener = (error: AgentRequestError) => void;\n\n/** Tell `listener` about `error`; a listener that throws never replaces the error itself. */\nexport function reportHttpError(\n listener: HttpErrorListener | undefined,\n error: AgentRequestError,\n): void {\n if (listener === undefined) return;\n try {\n listener(error);\n } catch {\n /* the request's own error is what the caller gets */\n }\n}\n","import type {\n AgentCatalogEntry,\n AgentClientConfig,\n ChatQueueState,\n MessageAttachment,\n MessageFeedback,\n ModelCatalogView,\n QuotaReport,\n SkillCatalogEntry,\n ThreadDetail,\n ThreadSummary,\n ToolCatalogEntry,\n} from '@dudousxd/nestjs-agent-core';\nimport type {\n AgentBackend,\n AgentConnection,\n AttachmentUploadStrategy,\n ChatStreamRequest,\n ChatStreamResponse,\n MessageFeedbackInput,\n QueuedMessageUpdate,\n QueuedSendResult,\n ResumeStreamRequest,\n ThreadPatch,\n UploadAttachmentOptions,\n} from './backend.js';\n\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n type HttpErrorListener,\n readErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from './http-error.js';\n\nexport type { ThreadPatch } from './backend.js';\nexport type { AgentRequestError, HttpErrorListener } from './http-error.js';\n\n/**\n * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can\n * branch (e.g. 403 → \"not your thread\", 429 → quota) instead of string-matching a generic Error,\n * and what the server said: `message` is the body's `message` when it sent one (so a hook that\n * shows `error.message` shows the server's words), `code` its machine-readable `code`, and `body`\n * the whole answer, parsed when it is JSON.\n */\nexport class AgentHttpError extends Error implements AgentRequestError {\n /** The answer's body: parsed JSON, else its text; `undefined` when empty. */\n readonly body: unknown;\n /** The body's `code` (`quota_exceeded`, …), when the server sent one. */\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(answer.message ?? `Agent request failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'AgentHttpError';\n this.body = answer.body;\n this.code = answer.code;\n }\n\n /** Read a refused `response`'s body into an error. */\n static async from(response: Response, method: string, path: string): Promise<AgentHttpError> {\n return new AgentHttpError(\n response.status,\n method,\n path,\n response.statusText,\n await readErrorResponse(response),\n );\n }\n}\n\nexport interface CancelResult {\n aborted: boolean;\n}\n\nexport interface OkResult {\n ok: boolean;\n}\n\nexport interface AgentClientOptions {\n /**\n * The server's origin, e.g. `https://api.example.com`. Defaults to `''` (same origin). The\n * agent's route prefix is {@link AgentClientOptions.path}, not part of this.\n */\n baseUrl?: string;\n /**\n * The agent's route prefix — `AgentModule`'s `path`, with any global prefix in front\n * (`'api/agent'`). Leading/trailing slashes are optional. Defaults to `'agent'`.\n */\n path?: string;\n /** Static headers merged into every request. */\n headers?: Record<string, string>;\n /**\n * Resolved per request — for short-lived bearer tokens, or a CSRF header read from a cookie\n * (`{ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }`), which has to be read at request time because\n * the server may rotate it.\n */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /**\n * Forwarded to fetch so cookie auth works. Same-origin requests send cookies by default; set\n * `'include'` when the API lives on another origin (and have it answer with credentialed CORS).\n */\n credentials?: RequestCredentials;\n /** Injectable for tests / non-browser runtimes. */\n fetch?: typeof fetch;\n /**\n * Called with every error answer (an {@link AgentHttpError}, or a `MediaUploadError` from a\n * `mediaAttachments()` upload) right before it is thrown — the place for app-wide reactions such\n * as \"401 → sign in again\". The error still reaches the caller. Not called for the `404` a\n * resume answers when nothing is streaming, which is an answer, not a failure.\n */\n onHttpError?: HttpErrorListener;\n /** Attachment uploads. */\n attachments?: {\n /**\n * How `uploadAttachment` uploads. Omitted → `POST <path>/attachments` (multipart). Pass\n * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through\n * nestjs-media, or your own {@link AttachmentUploadStrategy}.\n */\n upload?: AttachmentUploadStrategy;\n };\n}\n\n/** `'/api/agent'` from `'api/agent'`, `'/api/agent/'`, …; `''` for an empty path. */\nexport function normalizeAgentPath(path: string | undefined): string {\n const trimmed = (path ?? 'agent').replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `/${trimmed}`;\n}\n\nconst HEADER_RUN_ID = 'x-agent-run-id';\nconst HEADER_THREAD_ID = 'x-agent-thread-id';\n\n/**\n * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.\n * Used by `useAgentChat`, but standalone-usable (vanilla fetch, no React).\n */\nexport class AgentClient implements AgentBackend {\n constructor(private readonly options: AgentClientOptions = {}) {}\n\n /** `POST <path>/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */\n async openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse> {\n const response = await this.fetchImpl()(`${this.root()}/chat`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n body: JSON.stringify(request.body),\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 202) {\n // Queued behind a turn already running on the thread: JSON, not a stream.\n return { body: emptyStream(), queued: (await response.json()) as QueuedSendResult };\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'POST', `${this.agentPath()}/chat`);\n }\n return streamResponse(response);\n }\n\n /**\n * `POST <path>/chat` for a message that should wait in the thread's queue — the body carries\n * `mode: 'queue'` (or `'interrupt'`), which the server always answers with `202` JSON.\n */\n enqueueMessage(request: ChatStreamRequest): Promise<QueuedSendResult> {\n return this.request<QueuedSendResult>('POST', '/chat', {\n mode: 'queue',\n ...request.body,\n });\n }\n\n /** `GET <path>/threads/:id/queue` — the thread's waiting messages, and whether it drains. */\n getQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('GET', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `PATCH <path>/queue/:messageId` — change a waiting message's text/attachments, or move it. */\n updateQueuedMessage(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('PATCH', `/queue/${encodeURIComponent(messageId)}`, update);\n }\n\n /** `DELETE <path>/queue/:messageId`. */\n removeQueuedMessage(messageId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/queue/${encodeURIComponent(messageId)}`);\n }\n\n /**\n * `POST <path>/queue/:messageId/interrupt` — run a waiting message now, cancelling the running\n * turn for it. `runId` when nothing was running and it started; else `interrupting`.\n */\n interruptQueuedMessage(\n messageId: string,\n ): Promise<ChatQueueState & { runId?: string; interrupting?: string }> {\n return this.request<ChatQueueState & { runId?: string; interrupting?: string }>(\n 'POST',\n `/queue/${encodeURIComponent(messageId)}/interrupt`,\n );\n }\n\n /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */\n clearQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */\n resumeQueue(threadId: string): Promise<ChatQueueState & { runId?: string }> {\n return this.request<ChatQueueState & { runId?: string }>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/queue/resume`,\n );\n }\n\n /**\n * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is\n * streaming under that id (404).\n */\n async resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null> {\n const path = `/chat/${encodeURIComponent(request.runId)}/stream`;\n const query = request.after !== undefined && request.after > 0 ? `?after=${request.after}` : '';\n const response = await this.fetchImpl()(`${this.root()}${path}${query}`, {\n method: 'GET',\n headers: {\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 404) {\n return null;\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'GET', `${this.agentPath()}${path}`);\n }\n return streamResponse(response);\n }\n\n /**\n * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers\n * the stored rating.\n */\n setMessageFeedback(\n messageId: string,\n input: MessageFeedbackInput,\n ): Promise<{ feedback: MessageFeedback | null }> {\n return this.request<{ feedback: MessageFeedback | null }>(\n 'POST',\n `/messages/${encodeURIComponent(messageId)}/feedback`,\n input,\n );\n }\n\n listThreads(): Promise<ThreadSummary[]> {\n return this.request<ThreadSummary[]>('GET', '/threads');\n }\n\n /**\n * The skills this caller can invoke right now, scope-resolved — the same list, built by the same\n * call, that the model is offered, so what a user can type after a `/` and what the agent can\n * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to\n * one conversation; omitted, the server reads it as a brand-new thread.\n */\n listSkills(threadId?: string): Promise<SkillCatalogEntry[]> {\n const query = threadId === undefined ? '' : `?threadId=${encodeURIComponent(threadId)}`;\n return this.request<SkillCatalogEntry[]>('GET', `/skills${query}`);\n }\n\n /**\n * The tools this caller can reach through `agent` (the default agent when omitted), each with the\n * server-declared `presentation` a chat narrates it by — the same list the model is offered.\n * Prefer {@link useToolCatalog}, which fetches it once and shares it.\n */\n listTools(agent?: string): Promise<ToolCatalogEntry[]> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ToolCatalogEntry[]>('GET', `/tools${query}`);\n }\n\n getThread(id: string): Promise<ThreadDetail> {\n return this.request<ThreadDetail>('GET', `/threads/${encodeURIComponent(id)}`);\n }\n\n deleteThread(id: string): Promise<void> {\n return this.request<void>('DELETE', `/threads/${encodeURIComponent(id)}`);\n }\n\n forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary> {\n return this.request<ThreadSummary>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */\n updateThread(id: string, patch: ThreadPatch): Promise<OkResult> {\n return this.request<OkResult>('PATCH', `/threads/${encodeURIComponent(id)}`, patch);\n }\n\n /**\n * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —\n * mirrors the backend's `POST <path>/attachments`. The returned {@link MessageAttachment} is\n * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.\n */\n async uploadAttachment(\n file: File,\n options: UploadAttachmentOptions = {},\n ): Promise<MessageAttachment> {\n const upload = this.options.attachments?.upload;\n if (upload !== undefined) {\n return upload(file, options, this.connection());\n }\n // `fetch` cannot observe an upload's progress; XHR can. Only when someone is listening, and\n // never when a `fetch` was injected (tests, non-browser runtimes).\n if (\n options.onProgress !== undefined &&\n this.options.fetch === undefined &&\n typeof XMLHttpRequest !== 'undefined'\n ) {\n return this.uploadWithProgress(file, options);\n }\n const formData = new FormData();\n formData.append('file', file);\n const response = await this.fetchImpl()(`${this.root()}/attachments`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n },\n body: formData,\n ...this.credentials(),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n });\n const attachment = await this.handleResponse<MessageAttachment>(\n response,\n 'POST',\n `${this.agentPath()}/attachments`,\n );\n options.onProgress?.(1);\n return attachment;\n }\n\n private async uploadWithProgress(\n file: File,\n { signal, onProgress }: UploadAttachmentOptions,\n ): Promise<MessageAttachment> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n };\n const url = `${this.root()}/attachments`;\n return new Promise<MessageAttachment>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', url);\n for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value);\n // Same-origin requests carry cookies regardless; this is the cross-origin opt-in.\n xhr.withCredentials = this.options.credentials === 'include';\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable && event.total > 0) onProgress?.(event.loaded / event.total);\n };\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n const error = new AgentHttpError(\n xhr.status,\n 'POST',\n `${this.agentPath()}/attachments`,\n xhr.statusText,\n readErrorAnswer(xhr.responseText),\n );\n reportHttpError(this.options.onHttpError, error);\n reject(error);\n return;\n }\n onProgress?.(1);\n resolve(JSON.parse(xhr.responseText) as MessageAttachment);\n };\n xhr.onerror = () => reject(new TypeError('Network error while uploading the attachment'));\n xhr.onabort = () => reject(new DOMException('The upload was aborted', 'AbortError'));\n if (signal !== undefined) {\n if (signal.aborted) {\n reject(new DOMException('The upload was aborted', 'AbortError'));\n return;\n }\n signal.addEventListener('abort', () => xhr.abort(), { once: true });\n }\n const formData = new FormData();\n formData.append('file', file);\n xhr.send(formData);\n });\n }\n\n promoteThread(id: string): Promise<OkResult> {\n return this.request<OkResult>('POST', `/threads/${encodeURIComponent(id)}/promote`);\n }\n\n truncateFromMessage(threadId: string, messageId: string): Promise<OkResult> {\n return this.request<OkResult>(\n 'DELETE',\n `/threads/${encodeURIComponent(threadId)}/from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** `GET <path>/models?agent=` — the models this caller may pick, grouped by provider. */\n listModels(agent?: string): Promise<ModelCatalogView> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ModelCatalogView>('GET', `/models${query}`);\n }\n\n /** `GET <path>/agents` — the registered agents, the default one flagged. */\n listAgents(): Promise<AgentCatalogEntry[]> {\n return this.request<AgentCatalogEntry[]>('GET', '/agents');\n }\n\n /** `GET <path>/config` — attachment limits and upload mode, and which features are on. */\n getConfig(): Promise<AgentClientConfig> {\n return this.request<AgentClientConfig>('GET', '/config');\n }\n\n /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */\n getQuota(): Promise<QuotaReport> {\n return this.request<QuotaReport>('GET', '/quota');\n }\n\n cancelStream(runId: string): Promise<CancelResult> {\n return this.request<CancelResult>('POST', `/chat/${encodeURIComponent(runId)}/cancel`);\n }\n\n /**\n * `remember` approves later calls of the same tool in the same thread; `via` names the surface\n * the decision came through (the server records `'web'` when omitted).\n */\n approveToolCall(input: { toolCallId: string; remember?: boolean; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/approve', input);\n }\n\n rejectToolCall(input: { toolCallId: string; reason?: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/reject', input);\n }\n\n /**\n * Settle a parked question set. `answers` is questionId → chosen option values; a question left\n * out takes the pre-picked default the request carried, resolved server-side against the request\n * the run already holds. Omit the whole object and the user has confirmed every pre-picked\n * answer — which is the point of the surface, so it is a valid submission rather than a blank.\n * `via` names the surface the answer came through (the server records `'web'` when omitted).\n */\n answerToolCall(input: {\n toolCallId: string;\n answers?: Record<string, string[]>;\n via?: string;\n }): Promise<void> {\n return this.request<void>('POST', '/tool-call/answer', input);\n }\n\n /**\n * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same\n * values a confirmation would, and persists differently on purpose — only one of them is\n * evidence the user chose them.\n */\n skipToolCall(input: { toolCallId: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/skip', input);\n }\n\n private fetchImpl(): typeof fetch {\n return this.options.fetch ?? globalThis.fetch;\n }\n\n /** This client's connection, for an {@link AttachmentUploadStrategy}. */\n private connection(): AgentConnection {\n return {\n baseUrl: this.baseUrl(),\n path: this.agentPath(),\n headers: () => this.resolveHeaders(),\n fetch: this.fetchImpl(),\n ...this.credentials(),\n ...(this.options.onHttpError !== undefined ? { onHttpError: this.options.onHttpError } : {}),\n };\n }\n\n private baseUrl(): string {\n return (this.options.baseUrl ?? '').replace(/\\/+$/, '');\n }\n\n private agentPath(): string {\n return normalizeAgentPath(this.options.path);\n }\n\n /** Origin + agent path: what every route hangs off. */\n private root(): string {\n return `${this.baseUrl()}${this.agentPath()}`;\n }\n\n private async resolveHeaders(): Promise<Record<string, string>> {\n const dynamic = (await this.options.getHeaders?.()) ?? {};\n return { ...this.options.headers, ...dynamic };\n }\n\n private credentials(): { credentials?: RequestCredentials } {\n return this.options.credentials !== undefined ? { credentials: this.options.credentials } : {};\n }\n\n private async request<T>(method: string, route: string, body?: unknown): Promise<T> {\n const path = `${this.agentPath()}${route}`;\n const response = await this.fetchImpl()(`${this.baseUrl()}${path}`, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await this.resolveHeaders()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n ...this.credentials(),\n });\n return this.handleResponse<T>(response, method, path);\n }\n\n /** The error for a refused `response`, already reported to `onHttpError`. */\n private async failure(response: Response, method: string, path: string): Promise<AgentHttpError> {\n const error = await AgentHttpError.from(response, method, path);\n reportHttpError(this.options.onHttpError, error);\n return error;\n }\n\n private async handleResponse<T>(response: Response, method: string, path: string): Promise<T> {\n if (!response.ok) {\n throw await this.failure(response, method, path);\n }\n if (response.status === 204) return undefined as T;\n const text = await response.text();\n if (!text) return undefined as T;\n return JSON.parse(text) as T;\n }\n}\n\nfunction emptyStream(): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n}\n\nfunction streamResponse(response: Response): ChatStreamResponse {\n const body = response.body as ReadableStream<Uint8Array>;\n const runId = response.headers?.get(HEADER_RUN_ID) ?? undefined;\n const threadId = response.headers?.get(HEADER_THREAD_ID) ?? undefined;\n return {\n body,\n ...(runId ? { runId } : {}),\n ...(threadId ? { threadId } : {}),\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/media/index.ts","../src/http-error.ts","../src/client.ts"],"sourcesContent":["import type { MessageAttachment } from '@dudousxd/nestjs-agent-core';\nimport { streamChunks } from '@dudousxd/nestjs-media-client';\nimport type { AttachmentUploadStrategy, UploadAttachmentOptions } from '../backend.js';\nimport { type AgentClientOptions, normalizeAgentPath } from '../client.js';\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from '../http-error.js';\n\n/** Tuning for {@link mediaAttachments}; every field optional. */\nexport interface MediaAttachmentsOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB. */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/**\n * Resumable attachment uploads through `@dudousxd/nestjs-media`, in one line:\n *\n * ```tsx\n * <AgentProvider attachments={{ upload: mediaAttachments() }}>\n * ```\n *\n * Reuses the client's own connection (origin, path, headers, credentials), so it needs no\n * configuration. Also takes `new AgentClient({ attachments: { upload: mediaAttachments() } })`.\n */\nexport function mediaAttachments(options: MediaAttachmentsOptions = {}): AttachmentUploadStrategy {\n return (file, uploadOptions, connection) =>\n createMediaUpload({\n ...options,\n baseUrl: connection.baseUrl,\n path: connection.path,\n getHeaders: connection.headers,\n fetch: connection.fetch,\n ...(connection.credentials !== undefined ? { credentials: connection.credentials } : {}),\n ...(connection.onHttpError !== undefined ? { onHttpError: connection.onHttpError } : {}),\n })(file, uploadOptions);\n}\n\n/**\n * Where and how {@link createMediaUpload} talks to the server. The connection fields are\n * {@link AgentClientOptions}' — pass the same ones your `AgentClient` gets, so the tus requests\n * carry the same cookies / CSRF header / bearer token as the rest of the chat.\n */\nexport interface MediaUploadOptions extends AgentClientOptions {\n /** Bytes per tus `PATCH`. Default 5 MiB (nestjs-media-client's default). */\n chunkSize?: number;\n /** Attempts per chunk before the upload fails. Default 3. */\n retries?: number;\n}\n\n/** What `useAttachments({ upload })` and `AgentBackend.uploadAttachment` both take. */\nexport type AttachmentUpload = (\n file: File,\n options?: UploadAttachmentOptions,\n) => Promise<MessageAttachment>;\n\n/**\n * A non-2xx answer from the agent's upload routes. Carries what `AgentHttpError` does — `status`\n * (413 too large, 415 type refused, 404 not yours), the server's `message`, `code` and `body` —\n * as its own class because this subpath is bundled apart from the root entry, where an\n * `instanceof AgentHttpError` would not match a copy. Both satisfy `AgentRequestError`.\n */\nexport class MediaUploadError extends Error implements AgentRequestError {\n readonly body: unknown;\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(\n answer.message ?? `Attachment upload failed: ${method} ${path} → ${status} ${statusText}`,\n );\n this.name = 'MediaUploadError';\n this.body = answer.body;\n this.code = answer.code;\n }\n}\n\ninterface BeginResponse {\n mediaId: string;\n location: string;\n}\n\n/**\n * A resumable `upload` for `useAttachments`, backed by `@dudousxd/nestjs-media` on the server\n * (`AgentMediaAttachmentsModule` from `@dudousxd/nestjs-agent/media`):\n *\n * 1. `POST <path>/attachments/uploads` — the agent validates the file and opens a tus session the\n * actor owns;\n * 2. the bytes stream to nestjs-media's own tus endpoint in chunks (`streamChunks`), reporting\n * progress and honouring the abort signal;\n * 3. `POST <path>/attachments/uploads/:mediaId/complete` — the agent confirms the bytes landed and\n * answers the `MessageAttachment` the turn will reference by `mediaId`.\n *\n * An aborted or failed upload is discarded server-side (`DELETE`), best effort.\n */\nexport function createMediaUpload(options: MediaUploadOptions = {}): AttachmentUpload {\n const origin = (options.baseUrl ?? '').replace(/\\/$/, '');\n const base = `${origin}${normalizeAgentPath(options.path)}/attachments/uploads`;\n const baseFetch = (): typeof fetch => options.fetch ?? fetch;\n // Every request — the agent's and media's tus PATCHes — rides the same credentials mode.\n const fetchWithCredentials: typeof fetch = (input, init) =>\n baseFetch()(input, {\n ...init,\n ...(options.credentials !== undefined ? { credentials: options.credentials } : {}),\n });\n const headers = async (): Promise<Record<string, string>> => ({\n ...options.headers,\n ...((await options.getHeaders?.()) ?? {}),\n });\n const discard = (mediaId: string): void => {\n void (async () => {\n await fetchWithCredentials(`${base}/${encodeURIComponent(mediaId)}`, {\n method: 'DELETE',\n headers: await headers(),\n });\n })().catch(() => undefined);\n };\n const call = async <T>(method: string, url: string, body?: unknown): Promise<T> => {\n const response = await fetchWithCredentials(url, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await headers()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n });\n if (!response.ok) {\n const error = new MediaUploadError(\n response.status,\n method,\n url,\n response.statusText,\n await readErrorResponse(response),\n );\n reportHttpError(options.onHttpError, error);\n throw error;\n }\n return (await response.json()) as T;\n };\n\n return async (file, { signal, onProgress } = {}) => {\n signal?.throwIfAborted();\n const begun = await call<BeginResponse>('POST', base, {\n filename: file.name,\n contentType: file.type,\n size: file.size,\n });\n const onAbort = () => discard(begun.mediaId);\n signal?.addEventListener('abort', onAbort, { once: true });\n try {\n const location = /^https?:\\/\\//.test(begun.location)\n ? begun.location\n : `${origin}${begun.location}`;\n await streamChunks(location, file, {\n resume: false,\n fetchImpl: fetchWithCredentials,\n getHeaders: headers,\n ...(options.chunkSize !== undefined ? { chunkSize: options.chunkSize } : {}),\n ...(options.retries !== undefined ? { retries: options.retries } : {}),\n ...(signal !== undefined ? { signal } : {}),\n onProgress: (sent, total) => onProgress?.(total > 0 ? sent / total : 1),\n });\n signal?.throwIfAborted();\n const attachment = await call<MessageAttachment>(\n 'POST',\n `${base}/${encodeURIComponent(begun.mediaId)}/complete`,\n );\n onProgress?.(1);\n return attachment;\n } catch (error) {\n if (!signal?.aborted) discard(begun.mediaId);\n throw error;\n } finally {\n signal?.removeEventListener('abort', onAbort);\n }\n };\n}\n","/**\n * What a server said when it refused a request, read once so every error class of this package\n * reports it the same way. Bundled into each entry that throws (the root and `/media`), which is\n * why it holds no class of its own.\n */\nexport interface ErrorAnswer {\n /** The body, parsed as JSON when it is JSON, else the raw text; `undefined` when empty. */\n body: unknown;\n /** `body.message` — a string, or NestJS's list of validation messages joined with `; `. */\n message: string | undefined;\n /** `body.code`, the machine-readable reason (`quota_exceeded`, …), when the server sent one. */\n code: string | undefined;\n}\n\n/** Read an error answer's body text. Never throws: a body that cannot be read is just absent. */\nexport function readErrorAnswer(text: string | null | undefined): ErrorAnswer {\n if (text == null || text === '') return { body: undefined, message: undefined, code: undefined };\n let body: unknown = text;\n try {\n body = JSON.parse(text);\n } catch {\n return { body: text, message: undefined, code: undefined };\n }\n if (body === null || typeof body !== 'object')\n return { body, message: undefined, code: undefined };\n const record = body as { message?: unknown; code?: unknown };\n const message =\n typeof record.message === 'string' && record.message !== ''\n ? record.message\n : Array.isArray(record.message) &&\n record.message.length > 0 &&\n record.message.every((each) => typeof each === 'string')\n ? record.message.join('; ')\n : undefined;\n const code = typeof record.code === 'string' && record.code !== '' ? record.code : undefined;\n return { body, message, code };\n}\n\n/** {@link readErrorAnswer} over a `Response` whose status is not 2xx. */\nexport async function readErrorResponse(response: Response): Promise<ErrorAnswer> {\n let text: string | undefined;\n try {\n text = await response.text();\n } catch {\n text = undefined;\n }\n return readErrorAnswer(text);\n}\n\n/**\n * The shape both {@link import('./client.js').AgentHttpError} and `MediaUploadError` share, for\n * code that handles either without an `instanceof` (the two live in separate bundles).\n */\nexport interface AgentRequestError extends Error {\n status: number;\n method: string;\n path: string;\n body: unknown;\n code: string | undefined;\n}\n\n/** Called with every error answer before it is thrown — for app-wide reactions (401, 402, …). */\nexport type HttpErrorListener = (error: AgentRequestError) => void;\n\n/** Tell `listener` about `error`; a listener that throws never replaces the error itself. */\nexport function reportHttpError(\n listener: HttpErrorListener | undefined,\n error: AgentRequestError,\n): void {\n if (listener === undefined) return;\n try {\n listener(error);\n } catch {\n /* the request's own error is what the caller gets */\n }\n}\n","import type {\n ActionProposalMutationView,\n ActionProposalView,\n AgentCatalogEntry,\n AgentClientConfig,\n ChatQueueState,\n MessageAttachment,\n MessageFeedback,\n ModelCatalogView,\n QuotaReport,\n SkillCatalogEntry,\n ThreadDetail,\n ThreadSummary,\n ToolCatalogEntry,\n} from '@dudousxd/nestjs-agent-core';\nimport type {\n AgentBackend,\n AgentConnection,\n AttachmentUploadStrategy,\n ChatStreamRequest,\n ChatStreamResponse,\n MessageFeedbackInput,\n QueuedMessageUpdate,\n QueuedSendResult,\n ResumeStreamRequest,\n ThreadPatch,\n UploadAttachmentOptions,\n} from './backend.js';\n\nimport {\n type AgentRequestError,\n type ErrorAnswer,\n type HttpErrorListener,\n readErrorAnswer,\n readErrorResponse,\n reportHttpError,\n} from './http-error.js';\n\nexport type { ThreadPatch } from './backend.js';\nexport type { AgentRequestError, HttpErrorListener } from './http-error.js';\n\n/**\n * Thrown by {@link AgentClient} on a non-2xx response. Carries the HTTP `status` so callers can\n * branch (e.g. 403 → \"not your thread\", 429 → quota) instead of string-matching a generic Error,\n * and what the server said: `message` is the body's `message` when it sent one (so a hook that\n * shows `error.message` shows the server's words), `code` its machine-readable `code`, and `body`\n * the whole answer, parsed when it is JSON.\n */\nexport class AgentHttpError extends Error implements AgentRequestError {\n /** The answer's body: parsed JSON, else its text; `undefined` when empty. */\n readonly body: unknown;\n /** The body's `code` (`quota_exceeded`, …), when the server sent one. */\n readonly code: string | undefined;\n\n constructor(\n readonly status: number,\n readonly method: string,\n readonly path: string,\n statusText: string,\n answer: ErrorAnswer = { body: undefined, message: undefined, code: undefined },\n ) {\n super(answer.message ?? `Agent request failed: ${method} ${path} → ${status} ${statusText}`);\n this.name = 'AgentHttpError';\n this.body = answer.body;\n this.code = answer.code;\n }\n\n /** Read a refused `response`'s body into an error. */\n static async from(response: Response, method: string, path: string): Promise<AgentHttpError> {\n return new AgentHttpError(\n response.status,\n method,\n path,\n response.statusText,\n await readErrorResponse(response),\n );\n }\n}\n\nexport interface CancelResult {\n aborted: boolean;\n}\n\nexport interface OkResult {\n ok: boolean;\n}\n\nexport interface AgentClientOptions {\n /**\n * The server's origin, e.g. `https://api.example.com`. Defaults to `''` (same origin). The\n * agent's route prefix is {@link AgentClientOptions.path}, not part of this.\n */\n baseUrl?: string;\n /**\n * The agent's route prefix — `AgentModule`'s `path`, with any global prefix in front\n * (`'api/agent'`). Leading/trailing slashes are optional. Defaults to `'agent'`.\n */\n path?: string;\n /** Static headers merged into every request. */\n headers?: Record<string, string>;\n /**\n * Resolved per request — for short-lived bearer tokens, or a CSRF header read from a cookie\n * (`{ 'X-XSRF-TOKEN': readCookie('XSRF-TOKEN') }`), which has to be read at request time because\n * the server may rotate it.\n */\n getHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n /**\n * Forwarded to fetch so cookie auth works. Same-origin requests send cookies by default; set\n * `'include'` when the API lives on another origin (and have it answer with credentialed CORS).\n */\n credentials?: RequestCredentials;\n /** Injectable for tests / non-browser runtimes. */\n fetch?: typeof fetch;\n /**\n * Called with every error answer (an {@link AgentHttpError}, or a `MediaUploadError` from a\n * `mediaAttachments()` upload) right before it is thrown — the place for app-wide reactions such\n * as \"401 → sign in again\". The error still reaches the caller. Not called for the `404` a\n * resume answers when nothing is streaming, which is an answer, not a failure.\n */\n onHttpError?: HttpErrorListener;\n /** Attachment uploads. */\n attachments?: {\n /**\n * How `uploadAttachment` uploads. Omitted → `POST <path>/attachments` (multipart). Pass\n * `mediaAttachments()` from `@dudousxd/nestjs-agent-react/media` for resumable uploads through\n * nestjs-media, or your own {@link AttachmentUploadStrategy}.\n */\n upload?: AttachmentUploadStrategy;\n };\n}\n\n/** `'/api/agent'` from `'api/agent'`, `'/api/agent/'`, …; `''` for an empty path. */\nexport function normalizeAgentPath(path: string | undefined): string {\n const trimmed = (path ?? 'agent').replace(/^\\/+|\\/+$/g, '');\n return trimmed === '' ? '' : `/${trimmed}`;\n}\n\nconst HEADER_RUN_ID = 'x-agent-run-id';\nconst HEADER_THREAD_ID = 'x-agent-thread-id';\n\n/**\n * Framework-agnostic REST client for the nestjs-agent endpoints — the default {@link AgentBackend}.\n * Used by `useAgentChat`, but standalone-usable (vanilla fetch, no React).\n */\nexport class AgentClient implements AgentBackend {\n constructor(private readonly options: AgentClientOptions = {}) {}\n\n /** `POST <path>/chat` → the turn's SSE stream. Throws {@link AgentHttpError} on a non-2xx. */\n async openChatStream(request: ChatStreamRequest): Promise<ChatStreamResponse> {\n const response = await this.fetchImpl()(`${this.root()}/chat`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n body: JSON.stringify(request.body),\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 202) {\n // Queued behind a turn already running on the thread: JSON, not a stream.\n return { body: emptyStream(), queued: (await response.json()) as QueuedSendResult };\n }\n if (response.ok && response.headers.get('content-type')?.includes('application/json')) {\n const decision = (await response.json()) as NonNullable<\n ChatStreamResponse['proposalDecision']\n >;\n if (decision.proposalDecision !== undefined && typeof decision.threadId === 'string')\n return { body: emptyStream(), threadId: decision.threadId, proposalDecision: decision };\n throw new Error('Unexpected JSON chat response');\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'POST', `${this.agentPath()}/chat`);\n }\n return streamResponse(response);\n }\n\n /**\n * `POST <path>/chat` for a message that should wait in the thread's queue — the body carries\n * `mode: 'queue'` (or `'interrupt'`), which the server always answers with `202` JSON.\n */\n enqueueMessage(request: ChatStreamRequest): Promise<QueuedSendResult> {\n return this.request<QueuedSendResult>('POST', '/chat', {\n mode: 'queue',\n ...request.body,\n });\n }\n\n /** `GET <path>/threads/:id/queue` — the thread's waiting messages, and whether it drains. */\n getQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('GET', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `PATCH <path>/queue/:messageId` — change a waiting message's text/attachments, or move it. */\n updateQueuedMessage(messageId: string, update: QueuedMessageUpdate): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('PATCH', `/queue/${encodeURIComponent(messageId)}`, update);\n }\n\n /** `DELETE <path>/queue/:messageId`. */\n removeQueuedMessage(messageId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/queue/${encodeURIComponent(messageId)}`);\n }\n\n /**\n * `POST <path>/queue/:messageId/interrupt` — run a waiting message now, cancelling the running\n * turn for it. `runId` when nothing was running and it started; else `interrupting`.\n */\n interruptQueuedMessage(\n messageId: string,\n ): Promise<ChatQueueState & { runId?: string; interrupting?: string }> {\n return this.request<ChatQueueState & { runId?: string; interrupting?: string }>(\n 'POST',\n `/queue/${encodeURIComponent(messageId)}/interrupt`,\n );\n }\n\n /** `DELETE <path>/threads/:id/queue` — drop every waiting message. */\n clearQueue(threadId: string): Promise<ChatQueueState> {\n return this.request<ChatQueueState>('DELETE', `/threads/${encodeURIComponent(threadId)}/queue`);\n }\n\n /** `POST <path>/threads/:id/queue/resume` — lift a pause; `runId` when the head started. */\n resumeQueue(threadId: string): Promise<ChatQueueState & { runId?: string }> {\n return this.request<ChatQueueState & { runId?: string }>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/queue/resume`,\n );\n }\n\n /**\n * `GET <path>/chat/:runId/stream[?after=<seq>]` → the run's SSE stream, or `null` when nothing is\n * streaming under that id (404).\n */\n async resumeChatStream(request: ResumeStreamRequest): Promise<ChatStreamResponse | null> {\n const path = `/chat/${encodeURIComponent(request.runId)}/stream`;\n const query = request.after !== undefined && request.after > 0 ? `?after=${request.after}` : '';\n const response = await this.fetchImpl()(`${this.root()}${path}${query}`, {\n method: 'GET',\n headers: {\n accept: 'text/event-stream',\n ...(await this.resolveHeaders()),\n ...request.headers,\n },\n ...this.credentials(),\n ...(request.signal !== undefined ? { signal: request.signal } : {}),\n });\n if (response.status === 404) {\n return null;\n }\n if (!response.ok || !response.body) {\n throw await this.failure(response, 'GET', `${this.agentPath()}${path}`);\n }\n return streamResponse(response);\n }\n\n /**\n * Rate a message (`'up'`/`'down'`, optional comment) or clear its rating (`value: null`). Answers\n * the stored rating.\n */\n setMessageFeedback(\n messageId: string,\n input: MessageFeedbackInput,\n ): Promise<{ feedback: MessageFeedback | null }> {\n return this.request<{ feedback: MessageFeedback | null }>(\n 'POST',\n `/messages/${encodeURIComponent(messageId)}/feedback`,\n input,\n );\n }\n\n listThreads(): Promise<ThreadSummary[]> {\n return this.request<ThreadSummary[]>('GET', '/threads');\n }\n\n /**\n * The skills this caller can invoke right now, scope-resolved — the same list, built by the same\n * call, that the model is offered, so what a user can type after a `/` and what the agent can\n * reach cannot drift apart. `threadId` reaches the host's own resolver, which may scope a skill to\n * one conversation; omitted, the server reads it as a brand-new thread.\n */\n listSkills(threadId?: string): Promise<SkillCatalogEntry[]> {\n const query = threadId === undefined ? '' : `?threadId=${encodeURIComponent(threadId)}`;\n return this.request<SkillCatalogEntry[]>('GET', `/skills${query}`);\n }\n\n /**\n * The tools this caller can reach through `agent` (the default agent when omitted), each with the\n * server-declared `presentation` a chat narrates it by — the same list the model is offered.\n * Prefer {@link useToolCatalog}, which fetches it once and shares it.\n */\n listTools(agent?: string): Promise<ToolCatalogEntry[]> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ToolCatalogEntry[]>('GET', `/tools${query}`);\n }\n\n getThread(id: string): Promise<ThreadDetail> {\n return this.request<ThreadDetail>('GET', `/threads/${encodeURIComponent(id)}`);\n }\n\n deleteThread(id: string): Promise<void> {\n return this.request<void>('DELETE', `/threads/${encodeURIComponent(id)}`);\n }\n\n forkFromMessage(threadId: string, messageId: string): Promise<ThreadSummary> {\n return this.request<ThreadSummary>(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/fork-from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** General `PATCH <path>/threads/:threadId` — title and/or the thread's pinned default agent. */\n updateThread(id: string, patch: ThreadPatch): Promise<OkResult> {\n return this.request<OkResult>('PATCH', `/threads/${encodeURIComponent(id)}`, patch);\n }\n\n /**\n * Uploads a file (image/PDF) for a vision-capable model turn. Multipart, field name `file` —\n * mirrors the backend's `POST <path>/attachments`. The returned {@link MessageAttachment} is\n * what a caller then rides on `sendMessage({ text }, { body: { attachments: [...] } })`.\n */\n async uploadAttachment(\n file: File,\n options: UploadAttachmentOptions = {},\n ): Promise<MessageAttachment> {\n const upload = this.options.attachments?.upload;\n if (upload !== undefined) {\n return upload(file, options, this.connection());\n }\n // `fetch` cannot observe an upload's progress; XHR can. Only when someone is listening, and\n // never when a `fetch` was injected (tests, non-browser runtimes).\n if (\n options.onProgress !== undefined &&\n this.options.fetch === undefined &&\n typeof XMLHttpRequest !== 'undefined'\n ) {\n return this.uploadWithProgress(file, options);\n }\n const formData = new FormData();\n formData.append('file', file);\n const response = await this.fetchImpl()(`${this.root()}/attachments`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n },\n body: formData,\n ...this.credentials(),\n ...(options.signal !== undefined ? { signal: options.signal } : {}),\n });\n const attachment = await this.handleResponse<MessageAttachment>(\n response,\n 'POST',\n `${this.agentPath()}/attachments`,\n );\n options.onProgress?.(1);\n return attachment;\n }\n\n private async uploadWithProgress(\n file: File,\n { signal, onProgress }: UploadAttachmentOptions,\n ): Promise<MessageAttachment> {\n const headers: Record<string, string> = {\n accept: 'application/json',\n ...(await this.resolveHeaders()),\n };\n const url = `${this.root()}/attachments`;\n return new Promise<MessageAttachment>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open('POST', url);\n for (const [name, value] of Object.entries(headers)) xhr.setRequestHeader(name, value);\n // Same-origin requests carry cookies regardless; this is the cross-origin opt-in.\n xhr.withCredentials = this.options.credentials === 'include';\n xhr.upload.onprogress = (event) => {\n if (event.lengthComputable && event.total > 0) onProgress?.(event.loaded / event.total);\n };\n xhr.onload = () => {\n if (xhr.status < 200 || xhr.status >= 300) {\n const error = new AgentHttpError(\n xhr.status,\n 'POST',\n `${this.agentPath()}/attachments`,\n xhr.statusText,\n readErrorAnswer(xhr.responseText),\n );\n reportHttpError(this.options.onHttpError, error);\n reject(error);\n return;\n }\n onProgress?.(1);\n resolve(JSON.parse(xhr.responseText) as MessageAttachment);\n };\n xhr.onerror = () => reject(new TypeError('Network error while uploading the attachment'));\n xhr.onabort = () => reject(new DOMException('The upload was aborted', 'AbortError'));\n if (signal !== undefined) {\n if (signal.aborted) {\n reject(new DOMException('The upload was aborted', 'AbortError'));\n return;\n }\n signal.addEventListener('abort', () => xhr.abort(), { once: true });\n }\n const formData = new FormData();\n formData.append('file', file);\n xhr.send(formData);\n });\n }\n\n promoteThread(id: string): Promise<OkResult> {\n return this.request<OkResult>('POST', `/threads/${encodeURIComponent(id)}/promote`);\n }\n\n truncateFromMessage(threadId: string, messageId: string): Promise<OkResult> {\n return this.request<OkResult>(\n 'DELETE',\n `/threads/${encodeURIComponent(threadId)}/from/${encodeURIComponent(messageId)}`,\n );\n }\n\n /** `GET <path>/models?agent=` — the models this caller may pick, grouped by provider. */\n listModels(agent?: string): Promise<ModelCatalogView> {\n const query = agent === undefined ? '' : `?agent=${encodeURIComponent(agent)}`;\n return this.request<ModelCatalogView>('GET', `/models${query}`);\n }\n\n /** `GET <path>/agents` — the registered agents, the default one flagged. */\n listAgents(): Promise<AgentCatalogEntry[]> {\n return this.request<AgentCatalogEntry[]>('GET', '/agents');\n }\n\n /** `GET <path>/config` — attachment limits and upload mode, and which features are on. */\n getConfig(): Promise<AgentClientConfig> {\n return this.request<AgentClientConfig>('GET', '/config');\n }\n\n /** `GET <path>/quota` — the caller's budget windows and the one blocking sends, if any. */\n getQuota(): Promise<QuotaReport> {\n return this.request<QuotaReport>('GET', '/quota');\n }\n\n cancelStream(runId: string): Promise<CancelResult> {\n return this.request<CancelResult>('POST', `/chat/${encodeURIComponent(runId)}/cancel`);\n }\n\n /**\n * `remember` approves later calls of the same tool in the same thread; `via` names the surface\n * the decision came through (the server records `'web'` when omitted).\n */\n async listActionProposals({ threadId }: { threadId: string }): Promise<ActionProposalView[]> {\n const proposals = new Map<string, ActionProposalView>();\n let after: { createdAt: number; id: string } | undefined;\n for (;;) {\n const route = `/threads/${encodeURIComponent(threadId)}/action-proposals${\n after === undefined ? '' : `?after=${encodeURIComponent(JSON.stringify(after))}`\n }`;\n const response = await this.requestResponse('GET', route);\n const page = await this.handleResponse<unknown>(\n response,\n 'GET',\n `${this.agentPath()}${route}`,\n );\n if (!Array.isArray(page)) throw new TypeError('Invalid action proposal page');\n for (const proposal of page) proposals.set(proposal.id, proposal);\n const header = response.headers.get('X-Action-Proposals-Next');\n if (header === null) return [...proposals.values()];\n let next: unknown;\n try {\n next = JSON.parse(decodeURIComponent(header));\n } catch {\n throw new TypeError('Invalid action proposal cursor');\n }\n if (\n typeof next !== 'object' ||\n next === null ||\n !('createdAt' in next) ||\n typeof next.createdAt !== 'number' ||\n !Number.isSafeInteger(next.createdAt) ||\n !('id' in next) ||\n typeof next.id !== 'string' ||\n next.id.length === 0 ||\n next.id.length > 255 ||\n (after !== undefined &&\n (next.createdAt < after.createdAt ||\n (next.createdAt === after.createdAt && next.id <= after.id)))\n )\n throw new TypeError('Invalid or non-advancing action proposal cursor');\n after = { createdAt: next.createdAt, id: next.id };\n }\n }\n approveActionProposal({\n threadId,\n proposalId,\n remember,\n }: {\n threadId: string;\n proposalId: string;\n remember?: boolean;\n }): Promise<ActionProposalMutationView> {\n return this.request(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/action-proposals/${encodeURIComponent(proposalId)}/approve`,\n remember === undefined ? {} : { remember },\n );\n }\n rejectActionProposal({\n threadId,\n proposalId,\n reason,\n }: {\n threadId: string;\n proposalId: string;\n reason?: string;\n }): Promise<ActionProposalMutationView> {\n return this.request(\n 'POST',\n `/threads/${encodeURIComponent(threadId)}/action-proposals/${encodeURIComponent(proposalId)}/reject`,\n reason === undefined ? {} : { reason },\n );\n }\n\n approveToolCall(input: { toolCallId: string; remember?: boolean; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/approve', input);\n }\n\n rejectToolCall(input: { toolCallId: string; reason?: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/reject', input);\n }\n\n /**\n * Settle a parked question set. `answers` is questionId → chosen option values; a question left\n * out takes the pre-picked default the request carried, resolved server-side against the request\n * the run already holds. Omit the whole object and the user has confirmed every pre-picked\n * answer — which is the point of the surface, so it is a valid submission rather than a blank.\n * `via` names the surface the answer came through (the server records `'web'` when omitted).\n */\n answerToolCall(input: {\n toolCallId: string;\n answers?: Record<string, string[]>;\n via?: string;\n }): Promise<void> {\n return this.request<void>('POST', '/tool-call/answer', input);\n }\n\n /**\n * Decline to answer and let the agent proceed on its own pre-picked values. Lands on the same\n * values a confirmation would, and persists differently on purpose — only one of them is\n * evidence the user chose them.\n */\n skipToolCall(input: { toolCallId: string; via?: string }): Promise<void> {\n return this.request<void>('POST', '/tool-call/skip', input);\n }\n\n private fetchImpl(): typeof fetch {\n return this.options.fetch ?? globalThis.fetch;\n }\n\n /** This client's connection, for an {@link AttachmentUploadStrategy}. */\n private connection(): AgentConnection {\n return {\n baseUrl: this.baseUrl(),\n path: this.agentPath(),\n headers: () => this.resolveHeaders(),\n fetch: this.fetchImpl(),\n ...this.credentials(),\n ...(this.options.onHttpError !== undefined ? { onHttpError: this.options.onHttpError } : {}),\n };\n }\n\n private baseUrl(): string {\n return (this.options.baseUrl ?? '').replace(/\\/+$/, '');\n }\n\n private agentPath(): string {\n return normalizeAgentPath(this.options.path);\n }\n\n /** Origin + agent path: what every route hangs off. */\n private root(): string {\n return `${this.baseUrl()}${this.agentPath()}`;\n }\n\n private async resolveHeaders(): Promise<Record<string, string>> {\n const dynamic = (await this.options.getHeaders?.()) ?? {};\n return { ...this.options.headers, ...dynamic };\n }\n\n private credentials(): { credentials?: RequestCredentials } {\n return this.options.credentials !== undefined ? { credentials: this.options.credentials } : {};\n }\n\n private async request<T>(method: string, route: string, body?: unknown): Promise<T> {\n const response = await this.requestResponse(method, route, body);\n return this.handleResponse<T>(response, method, `${this.agentPath()}${route}`);\n }\n\n private async requestResponse(method: string, route: string, body?: unknown): Promise<Response> {\n const path = `${this.agentPath()}${route}`;\n const response = await this.fetchImpl()(`${this.baseUrl()}${path}`, {\n method,\n headers: {\n accept: 'application/json',\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n ...(await this.resolveHeaders()),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n ...this.credentials(),\n });\n return response;\n }\n\n /** The error for a refused `response`, already reported to `onHttpError`. */\n private async failure(response: Response, method: string, path: string): Promise<AgentHttpError> {\n const error = await AgentHttpError.from(response, method, path);\n reportHttpError(this.options.onHttpError, error);\n return error;\n }\n\n private async handleResponse<T>(response: Response, method: string, path: string): Promise<T> {\n if (!response.ok) {\n throw await this.failure(response, method, path);\n }\n if (response.status === 204) return undefined as T;\n const text = await response.text();\n if (!text) return undefined as T;\n return JSON.parse(text) as T;\n }\n}\n\nfunction emptyStream(): ReadableStream<Uint8Array> {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.close();\n },\n });\n}\n\nfunction streamResponse(response: Response): ChatStreamResponse {\n const body = response.body as ReadableStream<Uint8Array>;\n const runId = response.headers?.get(HEADER_RUN_ID) ?? undefined;\n const threadId = response.headers?.get(HEADER_THREAD_ID) ?? undefined;\n return {\n body,\n ...(runId ? { runId } : {}),\n ...(threadId ? { threadId } : {}),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as _dudousxd_nestjs_agent_core_genui from '@dudousxd/nestjs-agent-core/genui';
|
|
2
|
+
import { CatalogOptions, ComponentPresentation, ComponentDefinition } from '@dudousxd/nestjs-agent-core/genui';
|
|
3
|
+
import { ComponentType, ReactNode } from 'react';
|
|
4
|
+
import { h as GenuiRegistry } from './types-DZtFsWec.cjs';
|
|
5
|
+
|
|
6
|
+
interface ReactComponentRenderers<P extends object> {
|
|
7
|
+
react?: ComponentType<P>;
|
|
8
|
+
text?: (props: P) => string | Promise<string>;
|
|
9
|
+
/** Return each page's complete props. Each page is validated again before SSR. */
|
|
10
|
+
paginate?: (props: P, rowsPerPage: number) => readonly P[] | Promise<readonly P[]>;
|
|
11
|
+
}
|
|
12
|
+
/** App-scoped registry shared by GenuiProvider and static server rendering. */
|
|
13
|
+
declare function createReactComponentRegistry(options?: CatalogOptions): {
|
|
14
|
+
readonly catalog: _dudousxd_nestjs_agent_core_genui.Catalog;
|
|
15
|
+
readonly manifest: readonly _dudousxd_nestjs_agent_core_genui.ComponentManifest[];
|
|
16
|
+
readonly components: GenuiRegistry;
|
|
17
|
+
prepare: (presentation: ComponentPresentation<object>) => Promise<ComponentPresentation<Record<string, unknown>>>;
|
|
18
|
+
render: (presentation: ComponentPresentation<object>, channel: string) => Promise<unknown>;
|
|
19
|
+
register<P extends object>(definition: ComponentDefinition<P>, renderers: ReactComponentRenderers<P>): /*elided*/ any;
|
|
20
|
+
renderPages(presentation: ComponentPresentation<object>, size: number): Promise<ReactNode[]>;
|
|
21
|
+
paginate(presentation: ComponentPresentation<object>, size: number): Promise<ComponentPresentation[]>;
|
|
22
|
+
};
|
|
23
|
+
type ReactComponentRegistry = ReturnType<typeof createReactComponentRegistry>;
|
|
24
|
+
|
|
25
|
+
export { type ReactComponentRegistry as R, type ReactComponentRenderers as a, createReactComponentRegistry as c };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as _dudousxd_nestjs_agent_core_genui from '@dudousxd/nestjs-agent-core/genui';
|
|
2
|
+
import { CatalogOptions, ComponentPresentation, ComponentDefinition } from '@dudousxd/nestjs-agent-core/genui';
|
|
3
|
+
import { ComponentType, ReactNode } from 'react';
|
|
4
|
+
import { h as GenuiRegistry } from './types-DZtFsWec.js';
|
|
5
|
+
|
|
6
|
+
interface ReactComponentRenderers<P extends object> {
|
|
7
|
+
react?: ComponentType<P>;
|
|
8
|
+
text?: (props: P) => string | Promise<string>;
|
|
9
|
+
/** Return each page's complete props. Each page is validated again before SSR. */
|
|
10
|
+
paginate?: (props: P, rowsPerPage: number) => readonly P[] | Promise<readonly P[]>;
|
|
11
|
+
}
|
|
12
|
+
/** App-scoped registry shared by GenuiProvider and static server rendering. */
|
|
13
|
+
declare function createReactComponentRegistry(options?: CatalogOptions): {
|
|
14
|
+
readonly catalog: _dudousxd_nestjs_agent_core_genui.Catalog;
|
|
15
|
+
readonly manifest: readonly _dudousxd_nestjs_agent_core_genui.ComponentManifest[];
|
|
16
|
+
readonly components: GenuiRegistry;
|
|
17
|
+
prepare: (presentation: ComponentPresentation<object>) => Promise<ComponentPresentation<Record<string, unknown>>>;
|
|
18
|
+
render: (presentation: ComponentPresentation<object>, channel: string) => Promise<unknown>;
|
|
19
|
+
register<P extends object>(definition: ComponentDefinition<P>, renderers: ReactComponentRenderers<P>): /*elided*/ any;
|
|
20
|
+
renderPages(presentation: ComponentPresentation<object>, size: number): Promise<ReactNode[]>;
|
|
21
|
+
paginate(presentation: ComponentPresentation<object>, size: number): Promise<ComponentPresentation[]>;
|
|
22
|
+
};
|
|
23
|
+
type ReactComponentRegistry = ReturnType<typeof createReactComponentRegistry>;
|
|
24
|
+
|
|
25
|
+
export { type ReactComponentRegistry as R, type ReactComponentRenderers as a, createReactComponentRegistry as c };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/** The `ui` frame component a composed tree is pushed under (`GENUI_TREE_COMPONENT` of `@dudousxd/nestjs-agent-core/genui`). */
|
|
4
|
+
declare const GENUI_TREE_COMPONENT = "genui:tree";
|
|
5
|
+
/** One pushed component, normalized from whatever carried it (a transcript block, a `data-ui` part, a stored entry). */
|
|
6
|
+
interface GenerativeUIItem {
|
|
7
|
+
fallbackText?: string;
|
|
8
|
+
componentVersions?: Record<string, number>;
|
|
9
|
+
id: string;
|
|
10
|
+
component: string;
|
|
11
|
+
props: Record<string, unknown>;
|
|
12
|
+
version: number | null;
|
|
13
|
+
toolCallId: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** A node of a composed tree (`genui:tree` frames): `{ type, props, children? }`. */
|
|
16
|
+
interface GenerativeUIElement {
|
|
17
|
+
type: string;
|
|
18
|
+
props: Record<string, unknown>;
|
|
19
|
+
children?: GenerativeUIElement[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* An app's renderer for one component: it receives the component's props spread, plus `children`
|
|
23
|
+
* when it is a layout node in a tree. Any React component — the library never styles anything.
|
|
24
|
+
*/
|
|
25
|
+
type GenuiRenderer<P = any> = ComponentType<P & {
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
}>;
|
|
28
|
+
/** Component name → the app's renderer. */
|
|
29
|
+
type GenuiRegistry = Record<string, GenuiRenderer>;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a component the registry does not have — typically a tenant's own component, fetched for
|
|
32
|
+
* the exact `version` a message was rendered with. Return `null`/`undefined` for "no such
|
|
33
|
+
* component". May be async; results are cached per resolver, name and version.
|
|
34
|
+
*/
|
|
35
|
+
type ResolveComponent = (name: string, version: number | null) => GenuiRenderer | null | undefined | Promise<GenuiRenderer | null | undefined>;
|
|
36
|
+
interface GenuiIssueLike {
|
|
37
|
+
path: (string | number)[];
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
type ValidationLike = {
|
|
41
|
+
ok: true;
|
|
42
|
+
value: Record<string, unknown>;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
issues: GenuiIssueLike[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* What the renderer needs from a catalog to validate props before drawing them. A `Catalog` from
|
|
49
|
+
* `@dudousxd/nestjs-agent-core/genui` satisfies it; declared structurally so any catalog-shaped
|
|
50
|
+
* object does too.
|
|
51
|
+
*/
|
|
52
|
+
interface GenuiCatalogLike {
|
|
53
|
+
get?(name: string): {
|
|
54
|
+
version?: number;
|
|
55
|
+
} | undefined;
|
|
56
|
+
has(name: string): boolean;
|
|
57
|
+
validate(name: string, props: unknown): Promise<ValidationLike>;
|
|
58
|
+
validateSync?(name: string, props: unknown): ValidationLike | undefined;
|
|
59
|
+
}
|
|
60
|
+
interface GenerativeUIOptions {
|
|
61
|
+
registry: GenuiRegistry;
|
|
62
|
+
/** Validate props against it before rendering. Components the catalog does not know render unvalidated. */
|
|
63
|
+
catalog?: GenuiCatalogLike;
|
|
64
|
+
resolveComponent?: ResolveComponent;
|
|
65
|
+
/**
|
|
66
|
+
* Draws a composed tree frame (`genui:tree`) whole — e.g. through json-render (see the
|
|
67
|
+
* `/genui/json-render` subpath's `GenuiProvider`). Omitted → trees render node by node through
|
|
68
|
+
* `registry`.
|
|
69
|
+
*/
|
|
70
|
+
treeRenderer?: GenuiRenderer<{
|
|
71
|
+
root?: GenerativeUIElement;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
/** Why an item did not render. */
|
|
75
|
+
type GenerativeUIProblem = {
|
|
76
|
+
reason: 'unknown';
|
|
77
|
+
item: GenerativeUIItem;
|
|
78
|
+
} | {
|
|
79
|
+
reason: 'invalid';
|
|
80
|
+
item: GenerativeUIItem;
|
|
81
|
+
issues: GenuiIssueLike[];
|
|
82
|
+
} | {
|
|
83
|
+
reason: 'error';
|
|
84
|
+
item: GenerativeUIItem;
|
|
85
|
+
error: unknown;
|
|
86
|
+
};
|
|
87
|
+
type GenerativeUIState = {
|
|
88
|
+
status: 'ready';
|
|
89
|
+
item: GenerativeUIItem;
|
|
90
|
+
Component: GenuiRenderer;
|
|
91
|
+
props: Record<string, unknown>;
|
|
92
|
+
} | {
|
|
93
|
+
status: 'loading';
|
|
94
|
+
item: GenerativeUIItem;
|
|
95
|
+
} | ({
|
|
96
|
+
status: 'problem';
|
|
97
|
+
} & GenerativeUIProblem);
|
|
98
|
+
|
|
99
|
+
export { type GenerativeUIItem as G, type ResolveComponent as R, GENUI_TREE_COMPONENT as a, type GenerativeUIElement as b, type GenerativeUIOptions as c, type GenerativeUIProblem as d, type GenerativeUIState as e, type GenuiCatalogLike as f, type GenuiIssueLike as g, type GenuiRegistry as h, type GenuiRenderer as i };
|