@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;" />
389 lines (387 loc) • 13.4 kB
JavaScript
import "reflect-metadata";
import { logger } from "@copilotkit/shared";
//#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 — the
* runtime stamps this onto `agent.headers` after `identifyUser` resolves,
* and the auto-attach in `configureAgentForRequest` reads it back to gate
* MCP-server attachment and to populate the outbound `X-Cpki-User-Id`
* header on every MCP request. Not part of the public user API.
*
* @internal
*/
const INTELLIGENCE_USER_ID_HEADER = "x-cpki-user-id";
/**
* 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 });
* } 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";
}
};
var CopilotKitIntelligence = class {
#apiUrl;
#runnerWsUrl;
#clientWsUrl;
#apiKey;
#mcpServerEnabled;
#threadCreatedListeners = /* @__PURE__ */ new Set();
#threadUpdatedListeners = /* @__PURE__ */ new Set();
#threadDeletedListeners = /* @__PURE__ */ new Set();
constructor(config) {
const intelligenceWsUrl = normalizeIntelligenceWsUrl(config.wsUrl);
this.#apiUrl = config.apiUrl.replace(/\/$/, "");
this.#runnerWsUrl = deriveRunnerWsUrl(intelligenceWsUrl);
this.#clientWsUrl = deriveClientWsUrl(intelligenceWsUrl);
this.#apiKey = config.apiKey;
this.#mcpServerEnabled = config.mcpServer ?? 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;
}
ɵgetRunnerAuthToken() {
return this.#apiKey;
}
/** @internal Used by the runtime's auto-attach to populate `Authorization`. */
ɵgetApiKey() {
return this.#apiKey;
}
/** @internal Used by the runtime's auto-attach to gate MCP attachment. */
ɵisMcpServerEnabled() {
return this.#mcpServerEnabled;
}
async #request(method, path, body) {
const url = `${this.#apiUrl}${path}`;
const headers = {
Authorization: `Bearer ${this.#apiKey}`,
"Content-Type": "application/json"
};
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}`);
}
async ɵsubscribeToThreads(params) {
return this.#request("POST", "/api/threads/subscribe", { userId: 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) {
return (await this.#request("GET", `/api/threads/${encodeURIComponent(params.threadId)}`)).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 }),
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 }),
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) {
return this.#request("GET", `/api/threads/${encodeURIComponent(params.threadId)}/messages`);
}
/**
* 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)}`, { reason: `Deleted via CopilotKit runtime (userId=${params.userId}, agentId=${params.agentId})` });
this.#invokeLifecycleCallback("onThreadDeleted", params);
}
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;
}
};
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`;
return `${wsUrl}/runner`;
}
function deriveClientWsUrl(wsUrl) {
if (wsUrl.endsWith("/client")) return wsUrl;
if (wsUrl.endsWith("/runner")) return `${wsUrl.slice(0, -7)}/client`;
return `${wsUrl}/client`;
}
//#endregion
export { CopilotKitIntelligence, INTELLIGENCE_USER_ID_HEADER, PlatformRequestError };
//# sourceMappingURL=client.mjs.map