Skip to main content
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 transforms async functions into state machine objects with three methods:
  • step — Runs the state machine until it reaches an await expression, then returns the pending Promise
  • get — Serializes the entire state machine state as a JavaScript object for storage
  • set — Restores a previously serialized state
The Guild runtime handles calling 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 an await. 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 throws NotImplemented and the build fails. Fix the source.

Async generators

There is no workaround in compiled code — restructure to a regular async function that returns a batch, or accumulate results imperatively.

Labeled break / continue

Refactor to a boolean flag, an early 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.
Give each helper a unique name.

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 the await 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.
Workaround: await each promise sequentially. The compiler is built around single await expressions, not composition.
This serializes between the two awaits, so each call is independent.

for await ... of and async iterators

The compiler emits a plain 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.
Workarounds:
  1. 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:
  2. Inline factory logic at the call site rather than going through a factory that returns a function value:
  3. Persist the data, not the functions. Cross the await with the inputs and construct functions just-in-time on the synchronous side.
  4. 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:
  5. If you cannot eliminate a callback, call it before any await and 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.
Workaround: invoke async work sequentially with explicit 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)
  • RegExp
  • WeakMap, WeakSet
  • Class instances (new Foo(...) for any user-defined class)
  • Arbitrary external functions (see above)
Keep these inside a single step. If you must cross an 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.
Keep all code that crosses 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 to dependencies 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 generated switch ($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 in for-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.