UNPKG

agents

Version:

A home for your AI agents

177 lines (176 loc) 7.38 kB
import { camelCaseToKebabCase, isInternalJsStubProp } from "./utils.js"; //#region src/sub-routing.ts /** * Sub-agent routing primitives — external addressability for facets. * * The public surface: * - `routeSubAgentRequest(req, parent, options?)` — the sub-agent * analog of `routeAgentRequest`. Use in custom fetch handlers. * - `getSubAgentByName(parent, Cls, name)` — the sub-agent analog * of `getAgentByName`. Returns a typed RPC stub that proxies * method calls through the parent. No `.fetch()` support — * external HTTP/WS routing goes through `routeSubAgentRequest`. * * Internal: * - `parseSubAgentPath(url)` — URL → `{ childClass, childName, remainingPath }`. * - `forwardToFacet(req, parent, match)` — resolves `ctx.facets.get(...)` * on the parent and returns `facetStub.fetch(rewrittenReq)`. * * @experimental The API surface may change before stabilizing. */ /** * URL segment marking a parent↔child boundary. * * Exposed as a constant so callers can build URLs symbolically, but * not configurable — the routing layer matches on the literal `sub` * token everywhere (parent fetch, client, helpers). */ const SUB_PREFIX = "sub"; /** * Parse a URL and extract the first `/sub/{class}/{name}` segment, * if any. Recursive nesting is handled naturally: callers parse one * level at a time; the child then parses its own URL (which still * contains any deeper `/sub/...` markers). * * Names are URL-decoded. Classes are kebab-to-CamelCase converted * via a best-effort match against a provided lookup — pass * `ctx.exports` keys to get exact CamelCase; pass `undefined` for * a tolerant conversion without validation. * * Returns `null` when the URL doesn't contain the marker at a * recognized position, or when the marker has no following * class+name pair. */ function parseSubAgentPath(url, options) { const parts = new URL(url).pathname.split("/").filter(Boolean); for (let i = 0; i < parts.length; i++) { if (parts[i] !== "sub") continue; if (i + 2 >= parts.length) continue; const classSegment = parts[i + 1]; const nameSegment = parts[i + 2]; const childClass = resolveClassName(classSegment, options?.knownClasses); if (!childClass) continue; let childName; try { childName = decodeURIComponent(nameSegment); } catch { continue; } const remainingParts = parts.slice(i + 3); const remainingPath = remainingParts.length > 0 ? "/" + remainingParts.join("/") : "/"; return { childClass, childName, remainingPath }; } return null; } /** * Best-effort kebab-to-CamelCase match. If `knownClasses` is * provided, returns the matching CamelCase entry (or null if no * match). If not, performs a naive kebab→CamelCase conversion. */ function resolveClassName(segment, knownClasses) { if (knownClasses) return knownClasses.find((name) => camelCaseToKebabCase(name) === segment) ?? null; return segment.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(""); } /** * Route a request into a sub-agent via its parent DO. * * Use this in a custom fetch handler when your URL shape doesn't * match the `/agents/{class}/{name}` default — you identify and * fetch the parent yourself, then let this helper parse the * `/sub/{child}/...` tail and forward it. * * Runs `onBeforeSubAgent` on the parent DO (authorization / request * mutation / short-circuit response). * * For the default `/agents/...` URL shape, use `routeAgentRequest` * instead — it handles the parent lookup and this dispatch in one * call. * * @example * ```ts * export default { * async fetch(req, env) { * const { parentName, rest } = myCustomParse(req.url); * const parent = await getAgentByName(env.Inbox, parentName); * return routeSubAgentRequest(req, parent, { fromPath: rest }); * } * }; * ``` * * @experimental The API surface may change before stabilizing. */ async function routeSubAgentRequest(req, parent, options) { if (!parseSubAgentPath(options?.fromPath ? `http://placeholder${options.fromPath.startsWith("/") ? "" : "/"}${options.fromPath}` : req.url)) return new Response("Sub-agent path not found in request URL", { status: 400 }); const forwardUrl = options?.fromPath !== void 0 ? rewritePathname(req.url, options.fromPath) : req.url; const forwardInit = { method: req.method, headers: new Headers(req.headers) }; if (req.body && req.method !== "GET" && req.method !== "HEAD") forwardInit.body = await req.arrayBuffer(); const forwardReq = new Request(forwardUrl, forwardInit); return parent.fetch(forwardReq); } /** * Replace a URL's pathname (and optionally its search) while * preserving every other component. Matches how `_cf_forwardToFacet` * forwards requests — pathname is the only thing that changes by * default; if the replacement path carries its own query string, * that wins. */ function rewritePathname(url, fromPath) { const normalized = fromPath.startsWith("/") ? fromPath : `/${fromPath}`; const queryIdx = normalized.indexOf("?"); const pathOnly = queryIdx >= 0 ? normalized.slice(0, queryIdx) : normalized; const querySuffix = queryIdx >= 0 ? normalized.slice(queryIdx) : ""; const rewritten = new URL(url); rewritten.pathname = pathOnly; if (querySuffix) rewritten.search = querySuffix; return rewritten.toString(); } /** * Get a typed RPC stub for a sub-agent from outside the parent DO. * * The returned stub proxies method calls through the parent via a * stateless per-call bridge (caller → parent → facet), so each * method invocation costs one extra RPC hop. Works across parent * hibernation — no cached references to go stale. * * Limitations: * - RPC methods only. `.fetch()` is not supported (will throw). * Use `routeSubAgentRequest` for external HTTP/WS. * - Arguments and return values must be structured-cloneable, * same as any DO RPC call. * - Does not run `onBeforeSubAgent` on the parent — analogous to * `getAgentByName` not running `onBeforeConnect`. The caller is * assumed to have performed whatever access checks are needed. * * @example * ```ts * const inbox = await getAgentByName(env.MyInbox, userId); * const chat = await getSubAgentByName(inbox, MyChat, chatId); * await chat.addMessage({ role: "user", content: "hi" }); * ``` * * @experimental The API surface may change before stabilizing. */ async function getSubAgentByName(parent, cls, name) { if (name.includes("\0")) throw new Error(`Sub-agent name contains null character (\\0), which is reserved.`); const bridge = parent; const className = cls?.name; if (!className) throw new Error(`getSubAgentByName: could not determine class name from ${cls}. Ensure you are passing the class constructor (e.g. getSubAgentByName(parent, MyChat, name)), not a string or undefined.`); return new Proxy({}, { get(_target, prop) { if (isInternalJsStubProp(prop)) return void 0; if (typeof prop !== "string") return void 0; if (prop === "fetch") return () => { throw new Error("getSubAgentByName returns an RPC-only stub — .fetch() is not supported. Use routeSubAgentRequest() or the /agents/{parent}/{name}/sub/{child}/{name} URL for external HTTP/WS routing."); }; return async (...args) => bridge._cf_invokeSubAgent(className, name, prop, args); } }); } //#endregion export { SUB_PREFIX, getSubAgentByName, parseSubAgentPath, routeSubAgentRequest }; //# sourceMappingURL=sub-routing.js.map