chore: simplify cloudflare compatibility (#191)

This commit is contained in:
NathanFlurry 2026-02-23 19:31:53 +00:00
parent 03e06e956d
commit 4201bd204b
No known key found for this signature in database
GPG key ID: 6A5F43A4F3241BCA
10 changed files with 418 additions and 249 deletions

View file

@ -48,22 +48,36 @@ import {
const API_PREFIX = "/v1";
const FS_PATH = `${API_PREFIX}/fs`;
const DEFAULT_BASE_URL = "http://sandbox-agent";
const DEFAULT_REPLAY_MAX_EVENTS = 50;
const DEFAULT_REPLAY_MAX_CHARS = 12_000;
const EVENT_INDEX_SCAN_EVENTS_LIMIT = 500;
export interface SandboxAgentConnectOptions {
baseUrl: string;
interface SandboxAgentConnectCommonOptions {
headers?: HeadersInit;
persist?: SessionPersistDriver;
replayMaxEvents?: number;
replayMaxChars?: number;
token?: string;
}
export type SandboxAgentConnectOptions =
| (SandboxAgentConnectCommonOptions & {
baseUrl: string;
fetch?: typeof fetch;
})
| (SandboxAgentConnectCommonOptions & {
fetch: typeof fetch;
baseUrl?: string;
});
export interface SandboxAgentStartOptions {
fetch?: typeof fetch;
headers?: HeadersInit;
persist?: SessionPersistDriver;
replayMaxEvents?: number;
replayMaxChars?: number;
}
export interface SandboxAgentStartOptions extends Omit<SandboxAgentConnectOptions, "baseUrl" | "token"> {
spawn?: SandboxAgentSpawnOptions | boolean;
}
@ -443,18 +457,22 @@ export class SandboxAgent {
private readonly seedSessionEventIndexBySession = new Map<string, Promise<void>>();
constructor(options: SandboxAgentConnectOptions) {
this.baseUrl = options.baseUrl.replace(/\/$/, "");
const baseUrl = options.baseUrl?.trim();
if (!baseUrl && !options.fetch) {
throw new Error("baseUrl is required unless fetch is provided.");
}
this.baseUrl = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
this.token = options.token;
this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
const resolvedFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
if (!resolvedFetch) {
throw new Error("Fetch API is not available; provide a fetch implementation.");
}
this.fetcher = resolvedFetch;
this.defaultHeaders = options.headers;
this.persist = options.persist ?? new InMemorySessionPersistDriver();
this.replayMaxEvents = normalizePositiveInt(options.replayMaxEvents, DEFAULT_REPLAY_MAX_EVENTS);
this.replayMaxChars = normalizePositiveInt(options.replayMaxChars, DEFAULT_REPLAY_MAX_CHARS);
if (!this.fetcher) {
throw new Error("Fetch API is not available; provide a fetch implementation.");
}
}
static async connect(options: SandboxAgentConnectOptions): Promise<SandboxAgent> {
@ -468,7 +486,8 @@ export class SandboxAgent {
}
const { spawnSandboxAgent } = await import("./spawn.js");
const handle = await spawnSandboxAgent(spawnOptions, options.fetch ?? globalThis.fetch);
const resolvedFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
const handle = await spawnSandboxAgent(spawnOptions, resolvedFetch);
const client = new SandboxAgent({
baseUrl: handle.baseUrl,

View file

@ -137,6 +137,45 @@ describe("Integration: TypeScript SDK flat session API", () => {
await sdk.dispose();
});
it("uses custom fetch for both HTTP helpers and ACP session traffic", async () => {
const defaultFetch = globalThis.fetch;
if (!defaultFetch) {
throw new Error("Global fetch is not available in this runtime.");
}
const seenPaths: string[] = [];
const customFetch: typeof fetch = async (input, init) => {
const outgoing = new Request(input, init);
const parsed = new URL(outgoing.url);
seenPaths.push(parsed.pathname);
const forwardedUrl = new URL(`${parsed.pathname}${parsed.search}`, baseUrl);
const forwarded = new Request(forwardedUrl.toString(), outgoing);
return defaultFetch(forwarded);
};
const sdk = await SandboxAgent.connect({
token,
fetch: customFetch,
});
await sdk.getHealth();
const session = await sdk.createSession({ agent: "mock" });
const prompt = await session.prompt([{ type: "text", text: "custom fetch integration test" }]);
expect(prompt.stopReason).toBe("end_turn");
expect(seenPaths).toContain("/v1/health");
expect(seenPaths.some((path) => path.startsWith("/v1/acp/"))).toBe(true);
await sdk.dispose();
});
it("requires baseUrl when fetch is not provided", async () => {
await expect(SandboxAgent.connect({ token } as any)).rejects.toThrow(
"baseUrl is required unless fetch is provided.",
);
});
it("restores a session on stale connection by recreating and replaying history on first prompt", async () => {
const persist = new InMemorySessionPersistDriver({
maxEventsPerSession: 200,