babel-plugin-agent-compiler is an internal Babel plugin used by the Guild runtime to translate procedural TypeScript agent code into state machines that can be paused, serialized, and resumed.
This plugin is what enables AutomaticallyManagedStateAgent. It allows agents written as straightforward async functions to be suspended when waiting for user input or long-running operations, and resumed later — even after a runtime restart.
How it works
The compiler transformsasync functions into state machine objects with three methods:
step— Runs the state machine until it reaches anawaitexpression, then returns the pendingPromiseget— Serializes the entire state machine state as a JavaScript object for storageset— Restores a previously serialized state
get to persist state when an agent is suspended (for example, while waiting for user input), and set to restore it when the agent resumes.
Limitations
The compiler supports most TypeScript, but some constructs either fail at build time or compile cleanly and then produce wrong behavior across anawait.
These limitations apply only to code inside a "use agent" function body. LLMAgent agents and self-managed-state agents are not compiled and have none of these restrictions.
Fails at build time
The compiler throwsNotImplemented and the build fails. Fix the source.
Async generators
Labeled break / continue
return, or extract the inner loop to a helper that signals via its return value.
Destructuring for-in loops
Two nested functions sharing a name
Non-async nested functions are hoisted to closure level by name. Two declarations of the same name in sibling scopes collide silently — the second shadows the first.Compiles cleanly, fails at runtime
These compile cleanly. If the agent never suspends, they may even appear to work in testing. But once the state machine is serialized at theawait and resumed, behavior is wrong. The compiler does not warn you.
Rule of thumb: anything stored in a local variable that crosses an await must be JSON-serializable via @guildai/s11n. s11n natively handles primitives, plain objects, arrays, Map, Set, Date, plain Error, cycles, and shared references. Everything else is suspect.
Promise objects and Promise.all / .any / .race
The compiler tracks await expressions individually. Raw Promise values and the static composition methods on Promise cannot survive serialization at the await that consumes them.
await expressions, not composition.
for await ... of and async iterators
for-of loop, so each item is the unresolved Promise rather than its value, and any iterator-protocol awaits are skipped. The build does not warn.
If the data source can be enumerated synchronously, use for-of and await each item explicitly. If the source is genuinely streaming, you cannot consume it from a compiled agent — fetch the data in a non-compiled helper or pre-load into an array.
Externally-produced function values across await
Inline arrow and function expressions written directly in your source are hoisted into a $fns array and survive serialization. Function values that arrive from outside the compiled source do not — the compiler has no body to hoist.
- Wrap module-level or imported functions in an inline arrow. The arrow is a literal the compiler can hoist; the body resolves the external name at call time:
- Inline factory logic at the call site rather than going through a factory that returns a function value:
- Persist the data, not the functions. Cross the
awaitwith the inputs and construct functions just-in-time on the synchronous side. - Replace a callback parameter with a tagged-dispatch enum. Wrapping a parameter callback in an inline arrow does not help — the parameter itself is in a frame slot:
- If you cannot eliminate a callback, call it before any
awaitand store only its result.
MemberExpression call of a compiled async in an object or array
Async arrow and async function expressions compile into the state machine as call descriptors stored in a closure-level$fns array — they invoke correctly when called via a plain identifier callee. Calling one via member access (obj.fn(), arr[i]()) goes through a different code path that the compiler does not yet rewrite, so JavaScript invokes the descriptor’s throw stub directly: compiled async function called from outside the state machine.
new on a compiled async function
Calling new on a compiled async expression dispatches through a code path the compiler does not rewrite, so JavaScript invokes the descriptor’s throw stub. This is unusual code — just don’t.
Descriptor leaks to non-compiled JavaScript
A compiled async expression that escapes into non-compiled JS — passed as a callback to.map, setTimeout, Promise.all, etc. — gets invoked as a plain function and throws. The error message includes the source location of the original async expression so leaks are diagnosable.
awaits in the compiled function. If you need fan-out, build an array of inputs across the loop and process them with sequential awaits.
Other non-serializable values across await
The following are not serializable; storing them in a local that crosses an await will produce wrong behavior after restore:
Promise(see above)RegExpWeakMap,WeakSet- Class instances (
new Foo(...)for any user-defined class) - Arbitrary external functions (see above)
await, store the data needed to reconstruct them (the regex source string, the constructor args) and rebuild on the other side.
Module and dependency limits
No imports from local modules
The compiler only processes the file containing the agent. Async helpers in sibling.ts files are not compiled into the state machine and will not survive serialization.
await in the same file as the agent. Pure-sync helpers can live elsewhere as long as the values they return are serializable.
CJS / native modules cannot be used
Agent code runs in an ESM-only sandbox. Adding a CommonJS package todependencies will fail at runtime. Verify each dependency is ESM-compatible before adding it ("type": "module" in its package.json, or shipped as .mjs).
No source maps
The compiled state machine has no source-map relationship to your TypeScript source. Runtime stack traces point into the generatedswitch ($step) { ... }. When debugging, reproduce in a small standalone test and read the generated code if you must — npx babel agent.js --plugins @guildai/babel-plugin-agent-compiler will print the transformed output.
For-in loop destructuring
The compiler supports destructuring infor-in loop bindings, such as for (const { x } in obj) or for (const [a, b] in obj). The TypeScript type-checker rejects this syntax with error TS2491, even though it is valid JavaScript. Use // @ts-expect-error to suppress the error in .ts files:
The
for-in loop variable is always a string key. Destructuring it accesses properties of the key string, not values from the iterated object.This package is used internally by the Guild runtime. You do not need to install or configure it directly when building agents.