Upgrade: names that moved to their own entry points
Some names left the main entry points of @helix-agents/core and @helix-agents/runtime-cloudflare for entry points of their own, so that importing the main entry loads less: core's main entry no longer loads the test mocks or a YAML parser, and runtime-cloudflare's no longer loads the File Store workspace (@cloudflare/shell). Only import paths change: every moved name behaves as before. Two additions need no code change: runtime-cloudflare has a new /do entry for Durable Object hosts, and @helix-agents/llm-vercel loads ai on its first model call.
Why
A Worker evaluates every module it imports before it serves its first request. The test mocks, the SKILL.md parser and the File Store workspace are used by few Workers (the mocks by none in production), but every Worker that imported the main entry paid for them. See Cloudflare Workers: Startup Cost for the numbers.
What moved where
| Names | Old import | New import |
|---|---|---|
MockLLMAdapter, MockLLMAdapterStrictModeError, MOCK_NATIVE_OUTPUT_WITHOUT_FORMAT and the types MockLLMAdapterOptions, MockResponse, MockTextResponse, MockToolCallsResponse, MockStructuredOutputResponse, MockNativeOutputResponse, MockErrorResponse, MockReasoningBlock | @helix-agents/core | @helix-agents/core/testing/mocks (or @helix-agents/core/testing in a vitest test, see below) |
RoutingMockLLMAdapter and the types RoutingMockLLMAdapterOptions, RoutingKey | @helix-agents/core | @helix-agents/core/testing/mocks (or /testing) |
MockEmbeddingAdapter | @helix-agents/core | @helix-agents/core/testing/mocks (or /testing) |
ScriptedModel, ScriptedModelError, messageText, inProcessControl, holdAfterDeltas, scriptedTextDeltas and the types ScriptedModelOptions, ScriptedModelControl, ScriptedResponse, ScriptedCallRecord, NewScriptedCallRecord, ScriptTarget, ScenarioModel, ControlCallOrigin, HoldAfterDeltasResponse, PausedHoldAfterDeltas, OutstandingScriptedHold, DeliveryBarrier | @helix-agents/core | @helix-agents/core/testing/mocks (or /testing) |
parseSkillFile and the type ParseResult | @helix-agents/core | @helix-agents/core/skill-file |
| Every name above, re-exported by the umbrella package | @helix-agents/sdk | the @helix-agents/core entry named above (add @helix-agents/core to your own dependencies if it is not there) |
CloudflareFileStoreWorkspaceProvider, CloudflareFileStoreWorkspace, CloudflareFileStoreFileSystem and the type CloudflareFileStoreWorkspaceConfig | @helix-agents/runtime-cloudflare | @helix-agents/runtime-cloudflare/filestore (which also exports CloudflareFileStoreWorkspaceProviderOptions) |
There are no aliases: the old imports fail to type-check (Module '"@helix-agents/core"' has no exported member 'MockLLMAdapter'), so the compiler lists every place to change. A split import looks like this:
// Before
import { defineAgent, MockLLMAdapter } from '@helix-agents/core';
import {
createAgentServer,
AgentRegistry,
CloudflareFileStoreWorkspaceProvider,
} from '@helix-agents/runtime-cloudflare';
// After
import { defineAgent } from '@helix-agents/core';
import { MockLLMAdapter } from '@helix-agents/core/testing/mocks';
import { createAgentServer, AgentRegistry } from '@helix-agents/runtime-cloudflare';
import { CloudflareFileStoreWorkspaceProvider } from '@helix-agents/runtime-cloudflare/filestore';@helix-agents/skill-fs and @helix-agents/skill-cli already import parseSkillFile from /skill-file; nothing changes for their users.
Mocks follow the specifier. vi.mock('@helix-agents/core', …) (or jest.mock) replaces only what is imported from '@helix-agents/core'. A test whose mock factory supplies a moved name ({ ...actual, MockLLMAdapter: … }) must mock the new entry instead (vi.mock('@helix-agents/core/testing/mocks', …)), and code that now imports the name from the new entry no longer sees a mock of the old one.
Moving the imports with the codemod
The codemod that renames the schemas (app mode, see Upgrade: SDK schemas are lazy getters) moves these imports too, in the same run: a named import of a name in the table above, from its old import, goes to an import of its new entry, so import { defineAgent, MockLLMAdapter } from '@helix-agents/core' becomes the two imports in the example above. It moves the mocks to /testing/mocks, never to /testing.
- The old import keeps its other names and their comments. A comment on the same line as a moved name (before or after its comma, such as
// eslint-disable-line) goes with it to the end of its new import; a comment on a line of its own stays in the list. When code follows that import on the same line, a//comment is written as/* … */so it cannot comment the code out (or, if it contains*/, on its own line above the import). A directive such aseslint-disable-lineor@ts-ignorethat no longer sits where it applied is listed. - All the names a file moves to one entry end up in one import of it: the file's existing import of that entry if it has one (an old import left empty is removed), else an old import whose names all moved there (only its module changes), else a new import after the first old one, on one line, ending with
;only if the old import did, and with the file's line endings. - Run your formatter afterwards.
It lists for a hand edit (by hand, with file and line) what it does not move: a re-export (export { MockLLMAdapter } from '@helix-agents/core'), a member of a namespace import (core.MockLLMAdapter), a name read from a dynamic import() or require(), and a key of a vi.mock / jest.mock factory for the old entry. It also lists every import it moved from @helix-agents/sdk, because the new import needs @helix-agents/core in your dependencies.
/testing or /testing/mocks
Both export the same mock adapters and ScriptedModel family, as the same objects.
@helix-agents/core/testing/mocksexports only the mocks and imports nothing from vitest. Use it anywhere: a vitest test, a Worker that a test bundles and runs in workerd (Miniflare,@cloudflare/vitest-pool-workers, awrangler.test.tomltest server), a demo or script run withtsxornode.@helix-agents/core/testingalso exports the store, stream and remote-consumer contract suites (stateStoreContractTests,streamManagerContractTests, …) and other test helpers. Those are vitest factories, so this entry importsvitest: use it only inside a vitest test process. Bundled into a Worker, or run withtsx/node, it fails to load.
When in doubt, import the mocks from /testing/mocks.
/do for Durable Object hosts (optional)
@helix-agents/runtime-cloudflare/do is the Durable Object runtime alone. A Worker that hosts agents in Durable Objects can import from it instead of the main entry:
import { createAgentServer, AgentRegistry } from '@helix-agents/runtime-cloudflare/do';- It exports
createAgentServer,DurableObjectAgentBase, the DO stores (DOStateStore,DOStreamManager,DOUsageStore), the DO clients (DOFrontendExecutor,DOStateStoreClient,DOStreamManagerClient,DOPeerError),AgentRegistry,defaultAgentRegistry,AlarmScheduler,createRemoteAgentWorkerHandler,serviceBindingTransport,DOStubTransport,mapStatusToRemoteProtocol, and their types. - It excludes the Cloudflare Workflows runtime (
CloudflareAgentExecutor,AgentSteps,runAgentWorkflow,createWorkflowRunner, the workflow DTO schemas, the instance-id helpers) and does not load it. The workspaces have entries of their own: File Store at/filestore, Sandbox at/sandbox. The script workspace (CloudflareDynamicWorkerWorkspaceProvider) and shared utilities such aspollUntilare in the main entry only. - Nothing changes if you keep the main entry. It still exports everything
/doexports, as the same objects:instanceofholds whichever entry a class came from. - A module can import from both. A name that is only in the main entry (
CloudflareDynamicWorkerWorkspaceProvider,pollUntil, the Workflows runtime) stays in a main-entry import beside the/doimport.
If you switch, change the import statements and your tests' mocks, not every occurrence of the package name:
- Mocks follow the specifier.
vi.mock('@helix-agents/runtime-cloudflare', …)mocks only imports of the main entry. Once the code under test imports from/do, mock'@helix-agents/runtime-cloudflare/do'; a test whose modules import from both entries mocks both. - Keep the package name in vitest's
server.deps.inline. That list is matched against the installed package's file paths, and'@helix-agents/runtime-cloudflare'already covers/do. Replaced by'@helix-agents/runtime-cloudflare/do', it matches nothing: the package is no longer inlined, the module mocks and aliases your config applies to its imports stop reaching it, and test files fail to load with an error such asReceived protocol 'cloudflare:'.
How much it saves depends on the bundler. In Node, /do loads in about 59 ms against 68 ms for the main entry. A wrangler bundle already drops most of what a Worker does not use (the packages declare "sideEffects": false), so the gain there is smaller: the repository's DO example bundle shrank by about 50 KiB. In an application with two Workers that moved every import it could to /do (one Worker kept a main-entry import for CloudflareDynamicWorkerWorkspaceProvider), wrangler check startup changed by about 1 ms (medians of 5: 38.5 to 37.1 ms and 57.7 to 56.4 ms, within run-to-run noise) and the bundles by less than 1%. Switching is worth it for a Worker that can drop the main entry entirely; otherwise expect little.
ai loads on the first model call (no action needed)
@helix-agents/llm-vercel imports ai the first time an adapter calls the model, not when you import @helix-agents/llm-vercel, so importing it builds none of ai's schemas (it used to build 169 at import). The API is unchanged. Two things follow:
- If
aifails to load, the first model call fails with that error (through the adapter's normal error path) instead of the import; the next call tries to load it again. - Your own modules that import
aior@ai-sdk/*at the top level still load them at startup. Create providers inside a getter, as in Keeping your own code lazy; the import itself still evaluates the package.
Build layout
@helix-agents/runtime-cloudflare and @helix-agents/store-cloudflare now build with code splitting, like @helix-agents/core: each entry point is a small file that imports shared chunks (dist/chunk-*.js and .cjs). An entry loads only the chunks it reaches. A class is one object across a package's entries, so instanceof now also holds between @helix-agents/runtime-cloudflare/do-clients and the main entry, and between @helix-agents/store-cloudflare, /d1 and /stream (each entry used to carry its own copy). Importing files under dist/ directly was never supported; their names changed.