agents
Version:
A home for your AI agents
273 lines (271 loc) • 9.17 kB
JavaScript
import { t as MessageType } from "./types-BITaDFf-.js";
import { t as camelCaseToKebabCase } from "./utils-B49TmLCI.js";
import { usePartySocket } from "partysocket/react";
import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
//#region src/react.tsx
const queryCache = /* @__PURE__ */ new Map();
function createCacheKey(agentNamespace, name, deps) {
return JSON.stringify([
agentNamespace,
name || "default",
...deps
]);
}
function getCacheEntry(key) {
const entry = queryCache.get(key);
if (!entry) return void 0;
if (Date.now() >= entry.expiresAt) {
queryCache.delete(key);
return;
}
return entry;
}
function setCacheEntry(key, promise, cacheTtl) {
const entry = {
promise,
expiresAt: Date.now() + cacheTtl
};
queryCache.set(key, entry);
return entry;
}
function deleteCacheEntry(key) {
queryCache.delete(key);
}
/**
* Creates a proxy that wraps RPC method calls.
* Internal JS methods (toJSON, then, etc.) return undefined to avoid
* triggering RPC calls during serialization (e.g., console.log)
*/
function createStubProxy(call) {
return new Proxy({}, { get: (_target, method) => {
if (typeof method === "symbol" || method === "toJSON" || method === "then" || method === "catch" || method === "finally" || method === "valueOf" || method === "toString" || method === "constructor" || method === "prototype" || method === "$$typeof" || method === "@@toStringTag" || method === "asymmetricMatch" || method === "nodeType") return;
return (...args) => call(method, args);
} });
}
const _testUtils = {
queryCache,
setCacheEntry,
getCacheEntry,
deleteCacheEntry,
clearCache: () => queryCache.clear(),
createStubProxy,
createCacheKey
};
function useAgent(options) {
const agentNamespace = camelCaseToKebabCase(options.agent);
const { query, queryDeps, cacheTtl, ...restOptions } = options;
const pendingCallsRef = useRef(/* @__PURE__ */ new Map());
const cacheKey = useMemo(() => createCacheKey(agentNamespace, options.name, queryDeps || []), [
agentNamespace,
options.name,
queryDeps
]);
const cacheKeyRef = useRef(cacheKey);
cacheKeyRef.current = cacheKey;
const ttl = cacheTtl ?? 300 * 1e3;
const [cacheInvalidatedAt, setCacheInvalidatedAt] = useState(0);
const isAsyncQuery = query && typeof query === "function";
const [awaitingQueryRefresh, setAwaitingQueryRefresh] = useState(false);
const queryPromise = useMemo(() => {
if (!query || typeof query !== "function") return null;
const cached = getCacheEntry(cacheKey);
if (cached) return cached.promise;
const promise = query().catch((error) => {
console.error(`[useAgent] Query failed for agent "${options.agent}":`, error);
deleteCacheEntry(cacheKey);
throw error;
});
setCacheEntry(cacheKey, promise, ttl);
return promise;
}, [
cacheKey,
query,
options.agent,
ttl,
cacheInvalidatedAt
]);
useEffect(() => {
if (!queryPromise || ttl <= 0) return;
const entry = getCacheEntry(cacheKey);
if (!entry) return;
const timeUntilExpiry = entry.expiresAt - Date.now();
const timer = setTimeout(() => {
deleteCacheEntry(cacheKey);
setCacheInvalidatedAt(Date.now());
}, Math.max(0, timeUntilExpiry));
return () => clearTimeout(timer);
}, [
cacheKey,
queryPromise,
ttl
]);
let resolvedQuery;
if (query) if (typeof query === "function") {
const queryResult = use(queryPromise);
if (queryResult) {
for (const [key, value] of Object.entries(queryResult)) if (value !== null && value !== void 0 && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") console.warn(`[useAgent] Query parameter "${key}" is an object and will be converted to "[object Object]". Query parameters should be string, number, boolean, or null.`);
resolvedQuery = queryResult;
}
} else resolvedQuery = query;
useEffect(() => {
if (awaitingQueryRefresh && resolvedQuery !== void 0) setAwaitingQueryRefresh(false);
}, [awaitingQueryRefresh, resolvedQuery]);
const [identity, setIdentity] = useState({
name: options.name || "default",
agent: agentNamespace,
identified: false
});
const previousIdentityRef = useRef({
name: null,
agent: null
});
const readyRef = useRef(void 0);
const resetReady = () => {
let resolve;
readyRef.current = {
promise: new Promise((r) => {
resolve = r;
}),
resolve
};
};
if (!readyRef.current) resetReady();
const socketOptions = options.basePath ? {
basePath: options.basePath,
path: options.path,
query: resolvedQuery,
...restOptions
} : {
party: agentNamespace,
prefix: "agents",
room: options.name || "default",
path: options.path,
query: resolvedQuery,
...restOptions
};
const socketEnabled = !awaitingQueryRefresh && (restOptions.enabled ?? true);
const agent = usePartySocket({
...socketOptions,
enabled: socketEnabled,
onMessage: (message) => {
if (typeof message.data === "string") {
let parsedMessage;
try {
parsedMessage = JSON.parse(message.data);
} catch (_error) {
return options.onMessage?.(message);
}
if (parsedMessage.type === MessageType.CF_AGENT_IDENTITY) {
const oldName = previousIdentityRef.current.name;
const oldAgent = previousIdentityRef.current.agent;
const newName = parsedMessage.name;
const newAgent = parsedMessage.agent;
setIdentity({
name: newName,
agent: newAgent,
identified: true
});
readyRef.current?.resolve();
if (oldName !== null && oldAgent !== null && (oldName !== newName || oldAgent !== newAgent)) if (options.onIdentityChange) options.onIdentityChange(oldName, newName, oldAgent, newAgent);
else {
const agentChanged = oldAgent !== newAgent;
const nameChanged = oldName !== newName;
let changeDescription = "";
if (agentChanged && nameChanged) changeDescription = `agent "${oldAgent}" → "${newAgent}", instance "${oldName}" → "${newName}"`;
else if (agentChanged) changeDescription = `agent "${oldAgent}" → "${newAgent}"`;
else changeDescription = `instance "${oldName}" → "${newName}"`;
console.warn(`[agents] Identity changed on reconnect: ${changeDescription}. This can happen with server-side routing (e.g., basePath with getAgentByName) where the instance is determined by auth/session. Provide onIdentityChange callback to handle this explicitly, or ignore if this is expected for your routing pattern.`);
}
previousIdentityRef.current = {
name: newName,
agent: newAgent
};
options.onIdentity?.(newName, newAgent);
return;
}
if (parsedMessage.type === MessageType.CF_AGENT_STATE) {
options.onStateUpdate?.(parsedMessage.state, "server");
return;
}
if (parsedMessage.type === MessageType.CF_AGENT_MCP_SERVERS) {
options.onMcpUpdate?.(parsedMessage.mcp);
return;
}
if (parsedMessage.type === MessageType.RPC) {
const response = parsedMessage;
const pending = pendingCallsRef.current.get(response.id);
if (!pending) return;
if (!response.success) {
pending.reject(new Error(response.error));
pendingCallsRef.current.delete(response.id);
pending.stream?.onError?.(response.error);
return;
}
if ("done" in response) if (response.done) {
pending.resolve(response.result);
pendingCallsRef.current.delete(response.id);
pending.stream?.onDone?.(response.result);
} else pending.stream?.onChunk?.(response.result);
else {
pending.resolve(response.result);
pendingCallsRef.current.delete(response.id);
}
return;
}
}
options.onMessage?.(message);
},
onClose: (event) => {
resetReady();
setIdentity((prev) => ({
...prev,
identified: false
}));
if (isAsyncQuery) setAwaitingQueryRefresh(true);
deleteCacheEntry(cacheKeyRef.current);
setCacheInvalidatedAt(Date.now());
const error = /* @__PURE__ */ new Error("Connection closed");
for (const pending of pendingCallsRef.current.values()) {
pending.reject(error);
pending.stream?.onError?.("Connection closed");
}
pendingCallsRef.current.clear();
options.onClose?.(event);
}
});
const call = useCallback((method, args = [], streamOptions) => {
return new Promise((resolve, reject) => {
const id = crypto.randomUUID();
pendingCallsRef.current.set(id, {
reject,
resolve,
stream: streamOptions
});
const request = {
args,
id,
method,
type: MessageType.RPC
};
agent.send(JSON.stringify(request));
});
}, [agent]);
agent.setState = (state) => {
agent.send(JSON.stringify({
state,
type: MessageType.CF_AGENT_STATE
}));
options.onStateUpdate?.(state, "client");
};
agent.call = call;
agent.agent = identity.agent;
agent.name = identity.name;
agent.identified = identity.identified;
agent.ready = readyRef.current.promise;
agent.stub = createStubProxy(call);
if (identity.agent !== identity.agent.toLowerCase()) console.warn("Agent name: " + identity.agent + " should probably be in lowercase. Received: " + identity.agent);
return agent;
}
//#endregion
export { _testUtils, useAgent };
//# sourceMappingURL=react.js.map