> ## Documentation Index
> Fetch the complete documentation index at: https://docs.guild.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# babel-plugin-agent-compiler

> Babel plugin that compiles procedural TypeScript agents into resumable state machines.

`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

```typescript theme={null}
// ❌ NotImplemented: async generator function
async function* stream() { ... }
```

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

#### Labeled `break` / `continue`

```typescript theme={null}
// ❌ NotImplemented: break to label / continue to label
outer: for (...) {
  for (...) { break outer }
}
```

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

```typescript theme={null}
// ❌ NotImplemented: destructuring for-in loop
for (const { name } in things) { ... }

// ✅ Iterate the key, then destructure inside the body
for (const key in things) {
  const { name } = things[key]
  ...
}
```

#### 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.

```typescript theme={null}
// ❌ Second `bar` overwrites the first
function outer() {
  { function bar() { ... }; bar() }
  { function bar() { ... }; bar() }
}
```

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.

```typescript theme={null}
// ❌ Won't survive a suspend at the await
const p = fetchSomething();
await delay();
return await p;

// ❌ Same problem
const [a, b] = await Promise.all([fetchA(), fetchB()]);
const winner = await Promise.race([f1(), f2()]);
```

**Workaround:** await each promise sequentially. The compiler is built around single `await` expressions, not composition.

```typescript theme={null}
// ✅
const a = await fetchA();
const b = await fetchB();
```

This serializes between the two awaits, so each call is independent.

#### `for await ... of` and async iterators

```typescript theme={null}
// ❌ The `await` is silently dropped
for await (const item of asyncIterable) { ... }
```

The compiler emits a plain `for-of` loop, so each `item` is the unresolved Promise rather than its value, and any iterator-protocol `await`s 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.

```typescript theme={null}
// ❌ Function received as a parameter
async function run(callback: () => void) {
  'use agent';
  await delay();
  callback(); // undefined after restore
}

// ❌ Function returned from a non-compiled call
const f = makeAdder(x);
await delay();
return f(1); // undefined after restore

// ❌ Imported / module-level function stored in a frame slot
import { uncompiled } from './helpers';
const h = { fn: uncompiled };
await delay();
return h.fn(x); // undefined after restore

// ❌ Functions produced by .map / Object.assign / spread
const fns = items.map((i) => () => process(i));
await delay();
return fns[0](); // undefined after restore
```

**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:
   ```typescript theme={null}
   const f = (n: number) => uncompiled(n); // ✅
   const h = { fn: (n: number) => uncompiled(n) }; // ✅
   ```
2. Inline factory logic at the call site rather than going through a factory that returns a function value:
   ```typescript theme={null}
   const f = (n: number) => x + n; // ✅ instead of makeAdder(x)
   ```
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:
   ```typescript theme={null}
   async function run(strategy: 'upper' | 'lower', text: string) {
     'use agent';
     await delay();
     return strategy === 'upper' ? text.toUpperCase() : text.toLowerCase();
   }
   ```
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`.

```typescript theme={null}
// ❌ Runtime throw
const handlers = {
  onClick: async (x: number) => {
    await delay();
    return x + 1;
  },
};
return await handlers.onClick(5);

// ❌ Same shape
const fns = [async (x: number) => x + 1];
return await fns[0](5);

// ✅ Extract to a local identifier first
const fn = handlers.onClick;
return await fn(5);
```

#### `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.

```typescript theme={null}
// ❌ .map invokes the descriptor directly → throws
const results = items.map(async (x) => task.tools.http_get({ url: x }))

// ❌ setTimeout schedules the descriptor as a plain callback
setTimeout(async () => { ... }, 1000)

// ❌ Promise.all itself is also unsupported, and the IIFE descriptor leak
//    is what fails first when each entry is invoked
await Promise.all([asyncFn(), otherAsync()])
```

**Workaround:** invoke async work sequentially with explicit `await`s 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.

```typescript theme={null}
// ❌ Async helper imported from another file
import { fetchAndProcess } from './helpers';
// fetchAndProcess will run, but if it suspends, its frame is lost
```

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:

```typescript theme={null}
// @ts-expect-error TS2491
for (const { key } in obj) {
  // use key
}
```

<Note>
  The `for-in` loop variable is always a string key. Destructuring it accesses properties of the key string, not values from the iterated object.
</Note>

<Note>
  This package is used internally by the Guild runtime. You do not need to install or configure it directly when building agents.
</Note>
