UNPKG

@copilotkit/runtime

Version:

<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />

628 lines (626 loc) 24 kB
import "reflect-metadata"; import { logger } from "@copilotkit/shared"; import { randomUUID as randomUUID$1 } from "crypto"; //#region src/v2/runtime/intelligence-platform/client.ts /** * Header name carrying the per-call end-user identity that the CopilotKit * Intelligence `/mcp` endpoint requires. Internal CopilotKit machinery — * `attachIntelligenceEnterpriseLearning` resolves the user via `identifyUser` * and bakes this header onto the `MCPMiddleware`'s transport config, so every * outbound MCP request stamps `X-Cpki-User-Id: <userId>`. Not part of the * public user API. * * @internal */ const INTELLIGENCE_USER_ID_HEADER = "x-cpki-user-id"; /** * REST base URL of CopilotKit's managed Intelligence platform — the default * when {@link CopilotKitIntelligenceConfig.apiUrl} is omitted. */ const MANAGED_INTELLIGENCE_API_URL = "https://api.intelligence.copilotkit.ai"; /** * Websocket base URL of CopilotKit's managed Intelligence platform — the * default when {@link CopilotKitIntelligenceConfig.wsUrl} is omitted. * * A different host from {@link MANAGED_INTELLIGENCE_API_URL}: the API and * realtime planes are deployed separately. */ const MANAGED_INTELLIGENCE_WS_URL = "wss://realtime.intelligence.copilotkit.ai"; /** * Error thrown when an Intelligence platform HTTP request returns a non-2xx * status. Carries the HTTP {@link status} code so callers can branch on * specific failures (e.g. 404 for "not found", 409 for "conflict") without * parsing the error message string. * * @example * ```ts * try { * await intelligence.getThread({ threadId, userId }); * } catch (error) { * if (error instanceof PlatformRequestError && error.status === 404) { * // thread does not exist yet * } * } * ``` */ var PlatformRequestError = class extends Error { constructor(message, status) { super(message); this.status = status; this.name = "PlatformRequestError"; } }; /** * Client for the CopilotKit Intelligence Platform REST API. * * Construct the client once and pass it to any consumers that need it * (e.g. `CopilotRuntime`, `IntelligenceAgentRunner`): * * ```ts * import { CopilotKitIntelligence, CopilotRuntime } from "@copilotkit/runtime"; * * const intelligence = new CopilotKitIntelligence({ * apiKey: process.env.COPILOTKIT_API_KEY!, * }); * * const runtime = new CopilotRuntime({ * agents, * intelligence, * }); * ``` * * `apiUrl` and `wsUrl` default to CopilotKit's managed Intelligence platform. * Override both together to target a self-hosted or non-production deployment: * * ```ts * const intelligence = new CopilotKitIntelligence({ * apiUrl: "https://intelligence.internal", * wsUrl: "wss://realtime.intelligence.internal", * apiKey: process.env.COPILOTKIT_API_KEY!, * }); * ``` */ var CopilotKitIntelligence = class { #apiUrl; #runnerWsUrl; #clientWsUrl; #channelsWsUrl; #apiKey; #enterpriseLearningEnabled; #threadCreatedListeners = /* @__PURE__ */ new Set(); #threadUpdatedListeners = /* @__PURE__ */ new Set(); #threadDeletedListeners = /* @__PURE__ */ new Set(); constructor(config) { const configuredApiUrl = configuredUrl(config.apiUrl); const configuredWsUrl = configuredUrl(config.wsUrl); warnOnPartialHostOverride(configuredApiUrl, configuredWsUrl); const intelligenceWsUrl = normalizeIntelligenceWsUrl(configuredWsUrl ?? MANAGED_INTELLIGENCE_WS_URL); this.#apiUrl = (configuredApiUrl ?? MANAGED_INTELLIGENCE_API_URL).replace(/\/$/, ""); this.#runnerWsUrl = deriveRunnerWsUrl(intelligenceWsUrl); this.#clientWsUrl = deriveClientWsUrl(intelligenceWsUrl); this.#channelsWsUrl = deriveChannelsWsUrl(intelligenceWsUrl); this.#apiKey = config.apiKey; this.#enterpriseLearningEnabled = config.enableEnterpriseLearning ?? false; if (config.onThreadCreated) this.onThreadCreated(config.onThreadCreated); if (config.onThreadUpdated) this.onThreadUpdated(config.onThreadUpdated); if (config.onThreadDeleted) this.onThreadDeleted(config.onThreadDeleted); } /** * Register a listener invoked whenever a thread is created. * * Multiple listeners can be registered. Each call returns an unsubscribe * function that removes the listener when called. * * @param callback - Receives the newly created {@link ThreadSummary}. * @returns A function that removes this listener when called. * * @example * ```ts * const unsubscribe = intelligence.onThreadCreated((thread) => { * console.log("Thread created:", thread.id); * }); * // later… * unsubscribe(); * ``` */ onThreadCreated(callback) { this.#threadCreatedListeners.add(callback); return () => { this.#threadCreatedListeners.delete(callback); }; } /** * Register a listener invoked whenever a thread is updated (including archive). * * Multiple listeners can be registered. Each call returns an unsubscribe * function that removes the listener when called. * * @param callback - Receives the updated {@link ThreadSummary}. * @returns A function that removes this listener when called. */ onThreadUpdated(callback) { this.#threadUpdatedListeners.add(callback); return () => { this.#threadUpdatedListeners.delete(callback); }; } /** * Register a listener invoked whenever a thread is deleted. * * Multiple listeners can be registered. Each call returns an unsubscribe * function that removes the listener when called. * * @param callback - Receives the {@link ThreadDeletedPayload} identifying * the deleted thread. * @returns A function that removes this listener when called. */ onThreadDeleted(callback) { this.#threadDeletedListeners.add(callback); return () => { this.#threadDeletedListeners.delete(callback); }; } ɵgetApiUrl() { return this.#apiUrl; } ɵgetRunnerWsUrl() { return this.#runnerWsUrl; } ɵgetClientWsUrl() { return this.#clientWsUrl; } ɵgetChannelsWsUrl() { return this.#channelsWsUrl; } ɵgetRunnerAuthToken() { return this.#apiKey; } /** @internal Used by `attachIntelligenceEnterpriseLearning` to populate `Authorization`. */ ɵgetApiKey() { return this.#apiKey; } /** @internal Used by `attachIntelligenceEnterpriseLearning` to gate MCP attachment. */ ɵisEnterpriseLearningEnabled() { return this.#enterpriseLearningEnabled; } async #request(method, path, body, extraHeaders) { const url = `${this.#apiUrl}${path}`; const headers = { Authorization: `Bearer ${this.#apiKey}`, "Content-Type": "application/json", ...extraHeaders }; const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : void 0 }); if (!response.ok) { const text = await response.text().catch(() => ""); logger.error({ status: response.status, body: text, path }, "Intelligence platform request failed"); throw new PlatformRequestError(`Intelligence platform error ${response.status}: ${text || response.statusText}`, response.status); } const text = await response.text(); if (!text) return; return JSON.parse(text); } #invokeLifecycleCallback(callbackName, payload) { const listeners = callbackName === "onThreadCreated" ? this.#threadCreatedListeners : callbackName === "onThreadUpdated" ? this.#threadUpdatedListeners : this.#threadDeletedListeners; for (const callback of listeners) try { callback(payload); } catch (error) { logger.error({ err: error, callbackName, payload }, "Intelligence lifecycle callback failed"); } } /** * List all non-archived threads for a given user and agent. * * @param params.userId - User whose threads to list. * @param params.agentId - Agent whose threads to list. * @returns The thread list along with realtime subscription credentials. * @throws {@link PlatformRequestError} on non-2xx responses. */ async listThreads(params) { const query = { userId: params.userId, agentId: params.agentId }; if (params.includeArchived) query.includeArchived = "true"; if (params.limit != null) query.limit = String(params.limit); if (params.cursor) query.cursor = params.cursor; const qs = new URLSearchParams(query).toString(); return this.#request("GET", `/api/threads?${qs}`); } /** * List the given user's long-term memories, newest first. * * The platform scopes by the opaque app user supplied in the * `x-cpki-user-id` header (resolved by the runtime via `identifyUser`), * not a query param. Pass `includeInvalidated` to also return retired rows. * * @returns The `{ memories }` envelope the client memory store consumes. * @throws {@link PlatformRequestError} on non-2xx responses. */ async listMemories(params) { const qs = params.includeInvalidated ? "?includeInvalidated=true" : ""; return this.#request("GET", `/api/memories${qs}`, void 0, { [INTELLIGENCE_USER_ID_HEADER]: params.userId }); } /** * Create a memory for the given user (platform `POST /api/memories`). * @returns The stored memory; `absorbed` is true if the content was merged * into a near-duplicate rather than inserted as a new row. * @throws {@link PlatformRequestError} on non-2xx responses. */ async createMemory(params) { return this.#request("POST", `/api/memories`, { content: params.content, kind: params.kind, ...params.scope !== void 0 ? { scope: params.scope } : {}, sourceThreadIds: params.sourceThreadIds ?? [] }, { [INTELLIGENCE_USER_ID_HEADER]: params.userId }); } /** * Supersede an existing memory (platform `PATCH /api/memories/:id`). The * `:id` row is retired and a new memory with the supplied content is * inserted atomically. * @returns The new memory plus `retiredId` (the id of the retired row). * @throws {@link PlatformRequestError} on non-2xx responses (e.g. 404 when * `:id` is not a live, same-scope memory for this user). */ async updateMemory(params) { return this.#request("PATCH", `/api/memories/${encodeURIComponent(params.id)}`, { content: params.content, kind: params.kind, ...params.scope !== void 0 ? { scope: params.scope } : {}, sourceThreadIds: params.sourceThreadIds ?? [] }, { [INTELLIGENCE_USER_ID_HEADER]: params.userId }); } /** * Non-lossily retire (forget) a memory (platform `DELETE /api/memories/:id`). * @throws {@link PlatformRequestError} on non-2xx responses. */ async removeMemory(params) { await this.#request("DELETE", `/api/memories/${encodeURIComponent(params.id)}`, void 0, { [INTELLIGENCE_USER_ID_HEADER]: params.userId }); } /** * Semantically recall the given user's memories (platform `POST * /api/memories/recall`, hybrid RAG). Each returned memory carries a * relevance `score`. `scope` narrows to `"user"`/`"project"`; omitted → platform default. * @throws {@link PlatformRequestError} on non-2xx responses. */ async recallMemories(params) { return this.#request("POST", `/api/memories/recall`, { query: params.query, ...params.limit !== void 0 ? { limit: params.limit } : {}, ...params.scope !== void 0 ? { scope: params.scope } : {} }, { [INTELLIGENCE_USER_ID_HEADER]: params.userId }); } async ɵsubscribeToThreads(params) { return this.#request("POST", "/api/threads/subscribe", { userId: params.userId }); } /** * Mint memory-realtime join credentials (platform `POST * /api/memories/subscribe`). Returns both the single-use `joinToken` and the * per-user `joinCode` the client needs to build the * `user_meta:memories:<joinCode>` channel topic. When the platform also * resolves a project scope it returns optional `projectJoinToken` / * `projectJoinCode`; both are passed through verbatim (omitted when absent, * the silent-degrade contract). * * The user is supplied via the `x-cpki-user-id` header — the same way every * other memory endpoint (`listMemories`/`createMemory`/…) identifies the app * user — because the platform's memory routes resolve identity from that * header, not the body. (This differs from `ɵsubscribeToThreads`, whose * platform endpoint reads `userId` from the body.) * * @throws {@link PlatformRequestError} on non-2xx responses. */ async ɵsubscribeToMemories(params) { return this.#request("POST", "/api/memories/subscribe", void 0, { [INTELLIGENCE_USER_ID_HEADER]: params.userId }); } /** * Update thread metadata (e.g. name). * * Triggers the `onThreadUpdated` lifecycle callback on success. * * @returns The updated thread summary. * @throws {@link PlatformRequestError} on non-2xx responses. */ async updateThread(params) { const response = await this.#request("PATCH", `/api/threads/${encodeURIComponent(params.threadId)}`, { userId: params.userId, agentId: params.agentId, ...params.updates }); this.#invokeLifecycleCallback("onThreadUpdated", response.thread); return response.thread; } /** * Create a new thread on the platform. * * Triggers the `onThreadCreated` lifecycle callback on success. * * @returns The newly created thread summary. * @throws {@link PlatformRequestError} with status 409 if a thread with the * same `threadId` already exists. */ async createThread(params) { const response = await this.#request("POST", `/api/threads`, { threadId: params.threadId, userId: params.userId, agentId: params.agentId, ...params.name !== void 0 ? { name: params.name } : {} }); this.#invokeLifecycleCallback("onThreadCreated", response.thread); return response.thread; } /** * Fetch a single thread by ID. * * @returns The thread summary. * @throws {@link PlatformRequestError} with status 404 if the thread does * not exist. */ async getThread(params) { const qs = new URLSearchParams({ userId: params.userId }).toString(); return (await this.#request("GET", `/api/threads/${encodeURIComponent(params.threadId)}?${qs}`)).thread; } /** * Get an existing thread or create it if it does not exist. * * Handles the race where a concurrent request creates the thread between * the initial 404 and the subsequent `createThread` call by catching the * 409 Conflict and retrying the get. * * Triggers the `onThreadCreated` lifecycle callback when a new thread is * created. * * @returns An object containing the thread and a `created` flag indicating * whether the thread was newly created (`true`) or already existed (`false`). * @throws {@link PlatformRequestError} on non-2xx responses other than * 404 (get) and 409 (create race). */ async getOrCreateThread(params) { try { return { thread: await this.getThread({ threadId: params.threadId, userId: params.userId }), created: false }; } catch (error) { if (!(error instanceof PlatformRequestError && error.status === 404)) throw error; } try { return { thread: await this.createThread(params), created: true }; } catch (error) { if (error instanceof PlatformRequestError && error.status === 409) return { thread: await this.getThread({ threadId: params.threadId, userId: params.userId }), created: false }; throw error; } } /** * Fetch the full message history for a thread. * * @returns All persisted messages in chronological order. * @throws {@link PlatformRequestError} on non-2xx responses. */ async getThreadMessages(params) { const qs = new URLSearchParams({ userId: params.userId }).toString(); return this.#request("GET", `/api/threads/${encodeURIComponent(params.threadId)}/messages?${qs}`); } /** @internal Fetches one authorized managed Channel asset for history hydration. */ async ɵgetManagedChannelAsset(assetId) { const path = `/api/channels/files/${encodeURIComponent(assetId)}`; const response = await fetch(`${this.#apiUrl}${path}`, { method: "GET", headers: { Authorization: `Bearer ${this.#apiKey}` } }); if (!response.ok) { const text = await response.text().catch(() => ""); throw new PlatformRequestError(`Intelligence platform error ${response.status}: ${text || response.statusText}`, response.status); } const bytes = new Uint8Array(await response.arrayBuffer()); const mimeType = response.headers.get("content-type") ?? void 0; return { bytes, ...mimeType ? { mimeType } : {} }; } /** * Fetch the persisted AG-UI event stream for a thread. * * Backed by the platform's `GET /api/_inspect/threads/:id/events` * introspection endpoint (see Intelligence PR #144). Events are returned * in replay order across every run that targeted the thread. The * `_inspect/` prefix flags this as debug-only — production code paths * must not depend on it. * * @throws {@link PlatformRequestError} on non-2xx responses. */ async getThreadEvents(params) { return this.#request("GET", `/api/_inspect/threads/${encodeURIComponent(params.threadId)}/events`); } /** * Fetch the current agent state for a thread. * * Backed by the platform's `GET /api/_inspect/threads/:id/state` * introspection endpoint (see Intelligence PR #144). The platform folds * RFC 6902 STATE_DELTA events on top of the latest STATE_SNAPSHOT, so * the returned state reflects the thread's current state — not just the * last snapshot. The discriminated response distinguishes "no snapshot * persisted yet" from "snapshot present" so consumers can render the * correct empty state. * * @throws {@link PlatformRequestError} on non-2xx responses. */ async getThreadState(params) { return this.#request("GET", `/api/_inspect/threads/${encodeURIComponent(params.threadId)}/state`); } /** * Mark a thread as archived. * * Archived threads are excluded from {@link listThreads} results. * Triggers the `onThreadUpdated` lifecycle callback on success. * * @throws {@link PlatformRequestError} on non-2xx responses. */ async archiveThread(params) { const response = await this.#request("PATCH", `/api/threads/${encodeURIComponent(params.threadId)}`, { userId: params.userId, agentId: params.agentId, archived: true }); this.#invokeLifecycleCallback("onThreadUpdated", response.thread); } /** * Permanently delete a thread and its message history. * * This is irreversible. Triggers the `onThreadDeleted` lifecycle callback * on success. * * @throws {@link PlatformRequestError} on non-2xx responses. */ async deleteThread(params) { await this.#request("DELETE", `/api/threads/${encodeURIComponent(params.threadId)}`, { userId: params.userId, agentId: params.agentId, reason: `Deleted via CopilotKit runtime (userId=${params.userId}, agentId=${params.agentId})` }); this.#invokeLifecycleCallback("onThreadDeleted", params); } /** * Annotate a thread event on the Intelligence platform's general annotation * endpoint (`PUT /connector/annotate/:clientEventId`). * * This is the generalized replacement for the old * `PUT /connector/user-actions/record/:clientEventId` endpoint. It supports * multiple annotation types via the `type` discriminator: * - `"user_action"` — records a user UI interaction for the self-learning loop. * - `"set_learning_containers"` — sets the learning containers for a thread. * * `userId` must be resolved on the runtime side before calling this — the * platform prefixes it with the project id from the API key. * * Always hits the idempotent `PUT /connector/annotate/:clientEventId` * endpoint. A retry with the same `clientEventId` returns * `{ id: <original>, duplicate: true }` instead of creating a new row. * When `clientEventId` is omitted, a UUID is auto-generated for this call. * * @throws {@link PlatformRequestError} on non-2xx responses, OR when the * platform returns an empty 2xx body (which would otherwise corrupt the * caller's typed result). */ async annotate(params) { const clientEventId = params.clientEventId ?? randomUUID$1(); const path = `/connector/annotate/${encodeURIComponent(clientEventId)}`; const body = { type: params.type, userId: params.userId, threadId: params.threadId }; if (params.payload !== void 0) body.payload = params.payload; if (params.occurredAt !== void 0) body.occurredAt = params.occurredAt; const response = await this.#request("PUT", path, body); if (response == null) { logger.error({ path }, "annotate: Intelligence platform returned 200 with empty or null body"); throw new PlatformRequestError("annotate: empty or null response body from Intelligence platform", 502); } return response; } async ɵacquireThreadLock(params) { return this.#request("POST", `/api/threads/${encodeURIComponent(params.threadId)}/lock`, { runId: params.runId, userId: params.userId, agentId: params.agentId, ...params.lockKeyPrefix !== void 0 ? { lockKeyPrefix: params.lockKeyPrefix } : {}, ...params.ttlSeconds !== void 0 ? { ttlSeconds: params.ttlSeconds } : {} }); } async ɵcleanupThreadLock(params) { return this.#request("DELETE", `/api/threads/${encodeURIComponent(params.threadId)}/lock`, { runId: params.runId }); } async ɵrenewThreadLock(params) { return this.#request("PATCH", `/api/threads/${encodeURIComponent(params.threadId)}/lock`, { runId: params.runId, ttlSeconds: params.ttlSeconds, ...params.lockKeyPrefix !== void 0 ? { lockKeyPrefix: params.lockKeyPrefix } : {} }); } async ɵgetActiveJoinCode(params) { const qs = new URLSearchParams({ userId: params.userId }).toString(); return this.#request("GET", `/api/threads/${encodeURIComponent(params.threadId)}/join-code?${qs}`); } async ɵconnectThread(params) { return await this.#request("POST", `/api/threads/${encodeURIComponent(params.threadId)}/connect`, { userId: params.userId, agentId: params.agentId }) ?? null; } }; /** * Normalize a configured URL to "provided" or "not provided". A blank string * counts as not provided: these URLs are typically wired from environment * variables, and a declared-but-empty variable (`COPILOTKIT_INTELLIGENCE_URL=`, * common in generated `.env` files and container configs) arrives as `""`. Left * as-is it would produce host-relative requests instead of falling back to the * managed platform. */ function configuredUrl(url) { const trimmed = url?.trim(); return trimmed ? trimmed : void 0; } /** * Warn when exactly one of `apiUrl`/`wsUrl` is configured. The API and realtime * planes are separate hosts, so a lone override silently leaves the other plane * on CopilotKit's managed platform — a self-hosted API paired with the managed * gateway (or vice versa), which fails as a hang rather than an error. */ function warnOnPartialHostOverride(apiUrl, wsUrl) { if (apiUrl && !wsUrl) { logger.warn(`CopilotKitIntelligence: apiUrl is set to "${apiUrl}" but wsUrl is not, so wsUrl falls back to the managed default "${MANAGED_INTELLIGENCE_WS_URL}". The API and realtime planes are separate hosts — set both when pointing at a self-hosted deployment.`); return; } if (wsUrl && !apiUrl) logger.warn(`CopilotKitIntelligence: wsUrl is set to "${wsUrl}" but apiUrl is not, so apiUrl falls back to the managed default "${MANAGED_INTELLIGENCE_API_URL}". The API and realtime planes are separate hosts — set both when pointing at a self-hosted deployment.`); } function normalizeIntelligenceWsUrl(wsUrl) { return wsUrl.replace(/\/$/, ""); } function deriveRunnerWsUrl(wsUrl) { if (wsUrl.endsWith("/runner")) return wsUrl; if (wsUrl.endsWith("/client")) return `${wsUrl.slice(0, -7)}/runner`; if (wsUrl.endsWith("/channels")) return `${wsUrl.slice(0, -9)}/runner`; return `${wsUrl}/runner`; } function deriveClientWsUrl(wsUrl) { if (wsUrl.endsWith("/client")) return wsUrl; if (wsUrl.endsWith("/runner")) return `${wsUrl.slice(0, -7)}/client`; if (wsUrl.endsWith("/channels")) return `${wsUrl.slice(0, -9)}/client`; return `${wsUrl}/client`; } function deriveChannelsWsUrl(wsUrl) { if (wsUrl.endsWith("/channels")) return wsUrl; if (wsUrl.endsWith("/runner")) return `${wsUrl.slice(0, -7)}/channels`; if (wsUrl.endsWith("/client")) return `${wsUrl.slice(0, -7)}/channels`; return `${wsUrl}/channels`; } //#endregion export { CopilotKitIntelligence, INTELLIGENCE_USER_ID_HEADER, PlatformRequestError }; //# sourceMappingURL=client.mjs.map