Skip to content

Commit 5e59a1c

Browse files
committed
fix(agent-worker): defer heavy SDKs to lazy import to clear CF startup CPU limit
The agent-worker bundle eagerly evaluated ElevenLabs (6.5MB), Polar (2.2MB), Composio (1.2MB), FAL and Exa SDKs at isolate startup via the static tool registry import chain, exceeding Cloudflare's script-startup CPU limit (10021) so the worker could never deploy. Convert each to `import type` + an async factory using dynamic `import()`, so esbuild wraps them in lazy init wrappers and their module-init runs only on first tool use, not at startup. Also fix the production deploy ordering: gateway must deploy before agent because agent cross-script binds gateway's QuotaTracker DO (the defining script must exist first). Service bindings resolve lazily so there is no cycle. Remove dead Blaxel secret bindings (BL_API_KEY/REGION/WORKSPACE) from the webhooks-worker — unused since the Daytona migration and blocking its deploy.
1 parent fdd447c commit 5e59a1c

8 files changed

Lines changed: 71 additions & 55 deletions

File tree

.github/workflows/deploy-workers.yml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ concurrency:
1919
cancel-in-progress: false
2020

2121
jobs:
22-
deploy-agent:
22+
# Gateway MUST deploy first: it DEFINES the QuotaTracker Durable Object that the
23+
# agent worker cross-script binds to (CF requires the defining script to exist
24+
# before a consumer can bind its class). Gateway's own service binding to
25+
# cheatcode-agent resolves lazily, so gateway-first has no chicken-and-egg.
26+
deploy-gateway:
2327
# Only on a successful Static Checks run on main, or a manual dispatch.
2428
if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main') }}
2529
runs-on: ubuntu-latest
@@ -36,16 +40,16 @@ jobs:
3640
node-version: 22
3741
cache: pnpm
3842
- run: pnpm install --frozen-lockfile
39-
- run: pnpm turbo build --filter=@cheatcode/agent-worker
43+
- run: pnpm turbo build --filter=@cheatcode/gateway-worker
4044
- uses: cloudflare/wrangler-action@v3
4145
with:
4246
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
4347
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
44-
workingDirectory: apps/agent-worker
48+
workingDirectory: apps/gateway-worker
4549
command: deploy
4650

47-
deploy-gateway:
48-
needs: deploy-agent
51+
deploy-agent:
52+
needs: deploy-gateway
4953
runs-on: ubuntu-latest
5054
environment: production
5155
steps:
@@ -60,16 +64,16 @@ jobs:
6064
node-version: 22
6165
cache: pnpm
6266
- run: pnpm install --frozen-lockfile
63-
- run: pnpm turbo build --filter=@cheatcode/gateway-worker
67+
- run: pnpm turbo build --filter=@cheatcode/agent-worker
6468
- uses: cloudflare/wrangler-action@v3
6569
with:
6670
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
6771
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
68-
workingDirectory: apps/gateway-worker
72+
workingDirectory: apps/agent-worker
6973
command: deploy
7074

7175
deploy-webhooks:
72-
needs: deploy-gateway
76+
needs: deploy-agent
7377
runs-on: ubuntu-latest
7478
environment: production
7579
steps:

apps/webhooks-worker/src/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ export interface WebhooksEnv
4545
AGENT?: Fetcher;
4646
CLERK_WEBHOOK_SECRET?: WorkerSecret;
4747
CLERK_WEBHOOK_SIGNING_SECRET?: WorkerSecret;
48-
BL_API_KEY?: WorkerSecret;
49-
BL_REGION?: WorkerSecret;
50-
BL_WORKSPACE?: WorkerSecret;
5148
CLOUDFLARE_ACCOUNT_ID?: string;
5249
CLOUDFLARE_ANALYTICS_API_TOKEN?: WorkerSecret;
5350
COMPOSIO_API_KEY?: WorkerSecret;

apps/webhooks-worker/wrangler.jsonc

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,6 @@
2727
"store_id": "ba25994718db4707ab99a498e22eb5a6",
2828
"secret_name": "daytona-api-key"
2929
},
30-
{
31-
"binding": "BL_API_KEY",
32-
"store_id": "ba25994718db4707ab99a498e22eb5a6",
33-
"secret_name": "blaxel-api-key"
34-
},
35-
{
36-
"binding": "BL_REGION",
37-
"store_id": "ba25994718db4707ab99a498e22eb5a6",
38-
"secret_name": "blaxel-region"
39-
},
40-
{
41-
"binding": "BL_WORKSPACE",
42-
"store_id": "ba25994718db4707ab99a498e22eb5a6",
43-
"secret_name": "blaxel-workspace"
44-
},
4530
{
4631
"binding": "CLERK_WEBHOOK_SIGNING_SECRET",
4732
"store_id": "ba25994718db4707ab99a498e22eb5a6",

packages/agent-core/src/mastra/tools/composio-tool.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Composio } from "@composio/core";
21
import { createTool } from "@mastra/core/tools";
32
import { z } from "zod/v4";
43
import {
@@ -138,7 +137,11 @@ interface BoundedJson {
138137
truncated: boolean;
139138
}
140139

141-
function createComposioToolClient(apiKey: string): ComposioToolClient {
140+
// Dynamically imported so the ~1.2 MB Composio SDK stays out of the agent-worker
141+
// isolate's startup path (CF startup CPU limit). Only loaded when a Composio tool
142+
// actually fires.
143+
async function createComposioToolClient(apiKey: string): Promise<ComposioToolClient> {
144+
const { Composio } = await import("@composio/core");
142145
const composio = new Composio({ apiKey });
143146
return {
144147
execute: (slug, body) => composio.tools.execute(slug, body),
@@ -190,7 +193,7 @@ export async function listComposioTools(
190193
}
191194

192195
try {
193-
const toolClient = client ?? createComposioToolClient(runtime.apiKey);
196+
const toolClient = client ?? (await createComposioToolClient(runtime.apiKey));
194197
const tools = await toolClient.getTools(runtime.userId, { toolkits: [input.integration] });
195198
const bounded = boundedJson(tools, MAX_COMPOSIO_OUTPUT_CHARS);
196199
return composioListToolsOutputSchema.parse({
@@ -231,7 +234,7 @@ export async function executeComposioAction(
231234
}
232235

233236
try {
234-
const toolClient = client ?? createComposioToolClient(runtime.apiKey);
237+
const toolClient = client ?? (await createComposioToolClient(runtime.apiKey));
235238
const response = composioExecuteResponseSchema.parse(
236239
await toolClient.execute(input.toolSlug, executeBody(input, runtime.userId, connectionId)),
237240
);

packages/billing/src/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { APIError } from "@cheatcode/observability";
2-
import { Polar } from "@polar-sh/sdk";
2+
import type { Polar } from "@polar-sh/sdk";
33
import { z } from "zod";
44
import { PLAN_CATALOG, type PlanCatalogEntry } from "./catalog";
55

@@ -178,7 +178,7 @@ export interface SubscriptionActionResult {
178178
}
179179

180180
export async function createCheckoutUrl(input: CreateCheckoutUrlInput): Promise<string> {
181-
const response = await polarClient(input.accessToken).checkouts.create({
181+
const response = await (await polarClient(input.accessToken)).checkouts.create({
182182
allowDiscountCodes: true,
183183
externalCustomerId: input.userId,
184184
metadata: { userId: input.userId },
@@ -202,7 +202,9 @@ export async function createCustomerPortalUrl(
202202
externalCustomerId: input.externalCustomerId,
203203
...(input.returnUrl ? { returnUrl: input.returnUrl } : {}),
204204
};
205-
const response = await polarClient(input.accessToken).customerSessions.create(sessionInput);
205+
const response = await (await polarClient(input.accessToken)).customerSessions.create(
206+
sessionInput,
207+
);
206208
return parseCustomerPortalUrl(response);
207209
}
208210

@@ -214,7 +216,7 @@ export async function cancelSubscriptionAtPeriodEnd(
214216
...(input.reason ? { customerCancellationReason: input.reason } : {}),
215217
...(input.comment ? { customerCancellationComment: input.comment } : {}),
216218
};
217-
const response = await polarClient(input.accessToken).subscriptions.update({
219+
const response = await (await polarClient(input.accessToken)).subscriptions.update({
218220
id: input.subscriptionId,
219221
subscriptionUpdate,
220222
});
@@ -224,15 +226,15 @@ export async function cancelSubscriptionAtPeriodEnd(
224226
export async function reactivateSubscription(
225227
input: ReactivateSubscriptionInput,
226228
): Promise<SubscriptionActionResult> {
227-
const response = await polarClient(input.accessToken).subscriptions.update({
229+
const response = await (await polarClient(input.accessToken)).subscriptions.update({
228230
id: input.subscriptionId,
229231
subscriptionUpdate: { cancelAtPeriodEnd: false },
230232
});
231233
return parseSubscriptionAction(response);
232234
}
233235

234236
export async function updateCustomerProfile(input: UpdateCustomerProfileInput): Promise<void> {
235-
await polarClient(input.accessToken).customers.update({
237+
await (await polarClient(input.accessToken)).customers.update({
236238
customerUpdate: {
237239
email: input.email,
238240
...(input.name !== undefined ? { name: input.name } : {}),
@@ -332,13 +334,18 @@ function isoDateOrNow(value: Date | null | undefined): string {
332334
return (value ?? new Date()).toISOString();
333335
}
334336

335-
function polarClient(accessToken: string): Polar {
337+
// Dynamically imported so the 2.2 MB Polar SDK stays out of the importing
338+
// isolate's startup path (CF startup CPU limit). The agent-worker pulls this
339+
// package for pure entitlement math and must not pay the Polar parse cost; the
340+
// SDK loads only when a checkout/portal/subscription call is actually made.
341+
async function polarClient(accessToken: string): Promise<Polar> {
336342
if (accessToken.trim().length === 0) {
337343
throw new APIError(503, "unavailable_maintenance", "Polar access token is not configured", {
338344
hint: "Set POLAR_ACCESS_TOKEN in the gateway Worker environment.",
339345
retriable: false,
340346
});
341347
}
348+
const { Polar } = await import("@polar-sh/sdk");
342349
return new Polar({ accessToken });
343350
}
344351

packages/tools-media/src/elevenlabs.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { APIError } from "@cheatcode/observability";
2-
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
2+
import type { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
33
import { z } from "zod/v4";
44
import { storeBytesMediaArtifact } from "./artifacts";
55
import type { MediaRuntimeContext } from "./runtime";
@@ -36,15 +36,16 @@ const TranscriptionResponseSchema = z
3636
export async function executeElevenLabsTts(
3737
input: unknown,
3838
runtimeContext: MediaRuntimeContext,
39-
client: ElevenLabsClientLike = createElevenLabsClient(runtimeContext),
39+
client?: ElevenLabsClientLike,
4040
): Promise<ElevenLabsTtsOutput> {
41+
const resolvedClient = client ?? (await createElevenLabsClient(runtimeContext));
4142
const parsedInput = ElevenLabsTtsInputSchema.parse(input);
4243
const request: TextToSpeechRequest = {
4344
modelId: parsedInput.modelId,
4445
outputFormat: parsedInput.outputFormat,
4546
text: parsedInput.text,
4647
};
47-
const audio = await client.textToSpeech.convert(parsedInput.voiceId, request);
48+
const audio = await resolvedClient.textToSpeech.convert(parsedInput.voiceId, request);
4849
const contentType = outputFormatContentType(parsedInput.outputFormat);
4950
const artifact = await storeBytesMediaArtifact({
5051
contentType,
@@ -71,8 +72,9 @@ export async function executeElevenLabsTts(
7172
export async function executeElevenLabsTranscription(
7273
input: unknown,
7374
runtimeContext: MediaRuntimeContext,
74-
client: ElevenLabsClientLike = createElevenLabsClient(runtimeContext),
75+
client?: ElevenLabsClientLike,
7576
): Promise<ElevenLabsTranscriptionOutput> {
77+
const resolvedClient = client ?? (await createElevenLabsClient(runtimeContext));
7678
const parsedInput = ElevenLabsTranscriptionInputSchema.parse(input);
7779
const audio = await transcriptionAudio(parsedInput, runtimeContext);
7880
const request: SpeechToTextRequest = {
@@ -84,7 +86,9 @@ export async function executeElevenLabsTranscription(
8486
if (parsedInput.languageCode) {
8587
request["languageCode"] = parsedInput.languageCode;
8688
}
87-
const response = TranscriptionResponseSchema.parse(await client.speechToText.convert(request));
89+
const response = TranscriptionResponseSchema.parse(
90+
await resolvedClient.speechToText.convert(request),
91+
);
8892
return ElevenLabsTranscriptionOutputSchema.parse({
8993
languageCode: response.languageCode ?? response.language_code,
9094
modelId: parsedInput.modelId,
@@ -93,7 +97,13 @@ export async function executeElevenLabsTranscription(
9397
});
9498
}
9599

96-
function createElevenLabsClient(runtimeContext: MediaRuntimeContext): ElevenLabsClientLike {
100+
// Dynamically imported so the 6.5 MB ElevenLabs SDK is a lazy chunk — kept out
101+
// of the agent-worker isolate's startup path (CF startup CPU limit). Only loaded
102+
// when a TTS/STT tool actually fires.
103+
async function createElevenLabsClient(
104+
runtimeContext: MediaRuntimeContext,
105+
): Promise<ElevenLabsClientLike> {
106+
const { ElevenLabsClient } = await import("@elevenlabs/elevenlabs-js");
97107
return new ElevenLabsClient({
98108
apiKey: requireMediaProviderKey(runtimeContext, "elevenlabs"),
99109
});

packages/tools-media/src/fal.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { APIError } from "@cheatcode/observability";
2-
import { createFalClient } from "@fal-ai/client";
32
import { z } from "zod/v4";
43
import { storeRemoteMediaArtifact } from "./artifacts";
54
import type { MediaRuntimeContext } from "./runtime";
@@ -51,12 +50,13 @@ const FalVideoResponseSchema = z
5150
export async function executeFalImage(
5251
input: unknown,
5352
runtimeContext: MediaRuntimeContext,
54-
client: FalClientLike = createScopedFalClient(runtimeContext),
53+
client?: FalClientLike,
5554
): Promise<FalMediaOutput> {
55+
const resolvedClient = client ?? (await createScopedFalClient(runtimeContext));
5656
const parsedInput = FalImageInputSchema.parse(input);
5757
const response = FalImageResponseSchema.parse(
5858
unwrapFalData(
59-
await client.subscribe(parsedInput.modelId, { input: falImagePayload(parsedInput) }),
59+
await resolvedClient.subscribe(parsedInput.modelId, { input: falImagePayload(parsedInput) }),
6060
),
6161
);
6262
const artifact = await storeFalImageArtifact({
@@ -80,12 +80,15 @@ export async function executeFalImage(
8080
export async function executeFalImageEdit(
8181
input: unknown,
8282
runtimeContext: MediaRuntimeContext,
83-
client: FalClientLike = createScopedFalClient(runtimeContext),
83+
client?: FalClientLike,
8484
): Promise<FalMediaOutput> {
85+
const resolvedClient = client ?? (await createScopedFalClient(runtimeContext));
8586
const parsedInput = FalImageEditInputSchema.parse(input);
8687
const response = FalImageResponseSchema.parse(
8788
unwrapFalData(
88-
await client.subscribe(parsedInput.modelId, { input: falImageEditPayload(parsedInput) }),
89+
await resolvedClient.subscribe(parsedInput.modelId, {
90+
input: falImageEditPayload(parsedInput),
91+
}),
8992
),
9093
);
9194
const artifact = await storeFalImageArtifact({
@@ -109,12 +112,13 @@ export async function executeFalImageEdit(
109112
export async function executeFalVideo(
110113
input: unknown,
111114
runtimeContext: MediaRuntimeContext,
112-
client: FalClientLike = createScopedFalClient(runtimeContext),
115+
client?: FalClientLike,
113116
): Promise<FalMediaOutput> {
117+
const resolvedClient = client ?? (await createScopedFalClient(runtimeContext));
114118
const parsedInput = FalVideoInputSchema.parse(input);
115119
const response = FalVideoResponseSchema.parse(
116120
unwrapFalData(
117-
await client.subscribe(parsedInput.modelId, { input: falVideoPayload(parsedInput) }),
121+
await resolvedClient.subscribe(parsedInput.modelId, { input: falVideoPayload(parsedInput) }),
118122
),
119123
);
120124
if (!response.video) {
@@ -145,7 +149,10 @@ export async function executeFalVideo(
145149
});
146150
}
147151

148-
function createScopedFalClient(runtimeContext: MediaRuntimeContext): FalClientLike {
152+
// Dynamically imported so the FAL SDK stays out of the agent-worker isolate's
153+
// startup path (CF startup CPU limit). Only loaded when an image/video tool fires.
154+
async function createScopedFalClient(runtimeContext: MediaRuntimeContext): Promise<FalClientLike> {
155+
const { createFalClient } = await import("@fal-ai/client");
149156
return createFalClient({
150157
credentials: requireMediaProviderKey(runtimeContext, "fal"),
151158
});

packages/tools-research/src/exa.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { ContentsOptions, RegularSearchOptions } from "exa-js";
2-
import Exa from "exa-js";
32
import type { ResearchRuntimeContext } from "./runtime";
43
import { requireResearchProviderKey } from "./runtime";
54
import {
@@ -33,18 +32,22 @@ export interface ExaClientLike {
3332
export async function executeExaSearch(
3433
input: unknown,
3534
runtimeContext: ResearchRuntimeContext,
36-
client: ExaClientLike = createExaClient(runtimeContext),
35+
client?: ExaClientLike,
3736
): Promise<ExaSearchOutput> {
37+
const resolvedClient = client ?? (await createExaClient(runtimeContext));
3838
const parsedInput = ExaSearchInputSchema.parse(input);
39-
const response = await client.search(parsedInput.query, exaSearchOptions(parsedInput));
39+
const response = await resolvedClient.search(parsedInput.query, exaSearchOptions(parsedInput));
4040

4141
return ExaSearchOutputSchema.parse({
4242
requestId: response.requestId,
4343
results: response.results.map(normalizeExaResult),
4444
});
4545
}
4646

47-
function createExaClient(runtimeContext: ResearchRuntimeContext): ExaClientLike {
47+
// Dynamically imported so the Exa SDK stays out of the agent-worker isolate's
48+
// startup path (CF startup CPU limit). Only loaded when the research tool fires.
49+
async function createExaClient(runtimeContext: ResearchRuntimeContext): Promise<ExaClientLike> {
50+
const { default: Exa } = await import("exa-js");
4851
const client = new Exa(requireResearchProviderKey(runtimeContext, "exa"));
4952
return {
5053
search: (query, options) =>

0 commit comments

Comments
 (0)