# Migrate TS Decorator Sdk

> Migrating a TypeScript Golem agent from the removed decorator/base API (@agent + BaseAgent) to the current schema-driven defineAgent API. Use when porting an existing decorator-based TS agent, when you see @agent()/extends BaseAgent/@endpoint/@description code that no longer compiles, or when asked to modernize a TS component to the current SDK.

- Skill: `golemcloud/migrate-ts-decorator-sdk` (Agent Skill)
- Install (CLI): `npx skillmds@latest add golemcloud/migrate-ts-decorator-sdk`
- Raw SKILL.md: https://api.skillmd.com/api/skills/golemcloud/migrate-ts-decorator-sdk/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: golemcloud (https://skillmd.com/u/golemcloud)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/golemcloud/migrate-ts-decorator-sdk

---


# Migrating TypeScript Agents from the Decorator SDK

The decorator/base surface of `@golemcloud/golem-ts-sdk` (`@agent()` classes extending `BaseAgent`, `@golemcloud/golem-ts-typegen`) has been **removed**. The schema-driven API — `defineAgent({...}).implement({...})` — is now the TypeScript authoring surface. This skill maps every old construct to its current replacement.

Verify each API name against `sdks/ts/packages/golem-ts-sdk/src/` and
`sdks/ts/packages/golem-ts-sdk/src/host/` as you go; public APIs are exported from `src/index.ts`.
Migrate directly to the current API. Do not add decorator facades, compatibility wrappers, aliases,
or adapters that preserve the removed surface.

## Mental model shift

| Decorator/base | Current SDK |
|----------------|--------|
| `@agent()` class `extends BaseAgent` | `defineAgent({ name, id, methods })` (contract) + `.implement({ init, methods })` (behaviour) |
| `constructor(name) { super() }` params | `id` record on `defineAgent`; the values become the constructor parameters |
| private class fields (`this.value = 0`) | state object returned by `init()`; read/write via `this` in handlers |
| method signature types (`increment(): Promise<number>`) | `method({ input: {...}, returns: <schema> })` with Standard Schema |
| `this.getId()` | `this.getId()` (same, on the handler `this`) |
| `getPrincipal()` | `this.getPrincipal()` |
| injected `Principal` constructor/method param | a bare `s.principal()` parameter (auto-injected with the per-call caller principal) |

There are **no agent classes or decorators, and no decorator type-generation step during
authoring**. Handlers are plain functions whose `this` is the state. The SDK build still generates
and packages runtime/WIT artifacts as part of its normal toolchain.

## Imports

The decorator exports are gone. Remove them and the typegen package:

```ts
// OLD — delete
import { BaseAgent, agent, prompt, description, endpoint, readonly, Config, Secret, Result } from '@golemcloud/golem-ts-sdk';
// import ... from '@golemcloud/golem-ts-typegen';   // gone entirely

// NEW — import what you use
import { z } from 'zod';                              // or valibot / arktype
import { defineAgent, method, s, http, Result } from '@golemcloud/golem-ts-sdk';
```

`Result` still exists (host `Result.ok` / `Result.err`). `Config` and `Secret` as **constructor parameter types** are gone — config is now a `config` record on `defineAgent` and secrets are `s.secret(...)` markers surfaced as `Secret<T>` handles on `this.config`.

## tsconfig

Drop the decorator flags; keep bundler resolution:

```jsonc
{
  "compilerOptions": {
    "moduleResolution": "bundler",   // keep
    // "experimentalDecorators": true,   // REMOVE
    // "emitDecoratorMetadata": true,    // REMOVE
    "strict": true,
    "useDefineForClassFields": false
  }
}
```

## Agent class → defineAgent + implement

OLD:

```ts
@agent({ mount: '/counters/{name}' })
class CounterAgent extends BaseAgent {
  private readonly name: string;
  private value: number = 0;
  constructor(name: string) { super(); this.name = name; }

  @prompt('Increase the count by one')
  @description('Increases the count by one and returns the new value')
  @endpoint({ post: '/increment' })
  async increment(): Promise<number> {
    this.value += 1;
    return this.value;
  }
}
```

NEW:

```ts
import { z } from 'zod';
import { defineAgent, method, http } from '@golemcloud/golem-ts-sdk';

export const Counter = defineAgent({
  name: 'CounterAgent',
  id: { name: z.string() },                       // constructor param → id record
  http: http.mount('/counters/{name}'),           // @agent({ mount }) → http.mount
  methods: {
    increment: method({
      input: {},
      returns: z.number(),
      description: 'Increases the count by one and returns the new value',   // @description
      promptHint: 'Increase the count by one',                              // @prompt
      http: http.post('/increment'),                                        // @endpoint({ post })
    }),
  },
});

export const CounterImpl = Counter.implement({
  init: ({ id }) => ({ name: id.name, value: 0 }),  // class fields → init() state
  methods: {
    increment() { this.value += 1; return this.value; },  // `this` is the state
  },
});
```

Register the agent by importing its module from `src/main.ts` (`import './counter-agent.js';`) — there is no exported class for the runtime to find; `defineAgent`/`.implement` register at module load.

## Annotations → method metadata + HTTP

- `@description('...')` → `method({ description: '...' })`; agent-level `@description` → `defineAgent({ description })`.
- `@prompt('...')` → `method({ promptHint: '...' })`; agent-level → `defineAgent({ promptHint })`.
- `@readonly()` → `method({ readOnly: true })`; `@readonly({ cache, usesPrincipal })` → `method({ readOnly: { cache: 'no-cache' | 'until-write' | { ttlNanos }, usesPrincipal } })`.
- `@endpoint({ post: '/x' })` → `method({ http: http.post('/x') })`, with a mount declared via `defineAgent({ http: http.mount('/prefix/{idVar}', { cors, auth }) })`.
- `@agent({ mount, cors })` → `defineAgent({ http: http.mount(mount, { cors }) })`.

Verbs: `http.get/head/post/put/del/patch/options/connect/trace`, plus `http.custom(verb, path)`. Query binds via inline `?k={var}`; headers via `{ headers: { 'X-Name': 'param' } }`.

## Result<T,E> return → s.result(ok, err)

OLD `async m(): Promise<Result<T, E>>` returning `Result.ok`/`Result.err` becomes a typed `returns`:

```ts
divide: method({ input: { a: z.number(), b: z.number() }, returns: s.result(z.number(), z.string()) }),
// handler:
divide({ a, b }) { return b === 0 ? Result.err('div by zero') : Result.ok(a / b); }
```

The failure travels as a value inside the success payload (same semantics as the decorator SDK). `ok(...)` / `err(...)` are `Result.ok(...)` / `Result.err(...)`.

## Config<T> / Secret<T> constructor params → config record

OLD passed `Config<AgentConfig>` (with nested `Secret<T>` fields) as a constructor parameter. NEW declares a `config` record on `defineAgent`; wrap any secret field (at any depth) in `s.secret(inner)`:

```ts
export const ConfigAgent = defineAgent({
  name: 'ConfigAgent',
  id: { name: z.string() },
  config: {
    greeting: z.string(),                  // local  → this.config.greeting : string (read fresh)
    apiKey: s.secret(z.string()),          // secret → this.config.apiKey : Secret<string>
    nested: z.object({ a: z.string(), c: s.secret(z.object({ d: z.string() })) }),
  },
  methods: { keyTail: method({ input: {}, returns: z.string() }) },
});

export const ConfigAgentImpl = ConfigAgent.implement({
  init: () => ({}),
  methods: {
    keyTail() { return this.config.apiKey.get().slice(-4); },   // Secret<T>.get() reveals fresh
  },
});
```

Local fields read their decoded value directly off `this.config`; secret fields are lazy log-safe `Secret<T>` handles — call `.get()` at the point of use, never log them. Only object schemas are flattened into nested config fields; unions/arrays/maps are read whole.

## save/loadSnapshot overrides → snapshotting option

OLD overrode `saveSnapshot()` / `loadSnapshot()` on the `BaseAgent` subclass. NEW is declarative on `defineAgent`:

```ts
// Typed, scoped: only the declared fields of `this` are serialized.
snapshotting: { state: z.object({ count: z.number() }), policy: { everyNInvocations: 5 } },
```

Policy: `'disabled'` (default) | `'default'` | `{ everyNInvocations: n }` | `{ periodicSeconds: n }`. A bare policy (no `state`) falls back to reflective JSON serialization of the whole state.

For fully custom bytes, supply a `snapshot` block on `.implement(...)` — `this` is the state:

```ts
Def.implement({
  init: () => ({ /* ... */ }),
  methods: { /* ... */ },
  snapshot: {
    save() { return new Uint8Array(/* serialize this */); },
    load(bytes) { /* restore this from bytes */ },
  },
});
```

Use method syntax for custom snapshot callbacks when they access state; arrow functions do not bind
the implementation's state as `this`.

## Current Replacements and Known Deltas

Use the current forms below and verify them against the SDK source rather than reconstructing the
removed decorator surface.

**Current forms:**
- **`readOnly` cache policies.** `@readonly({ cache: 'no-cache' | 'until-write' | { ttl } })` → `method({ readOnly: { cache: 'no-cache' | 'until-write' | { ttlNanos: <bigint> }, usesPrincipal?: boolean } })`. Bare `readOnly: true` uses the `until-write` policy (the base default); principal-dependent caching → `usesPrincipal: true`.
- **Config-on-RPC (`getWithConfig`).** `Agent.getWithConfig(id, overrides)` →
  `Def.client.get(id, overrides)`. For an existing phantom agent, use
  `Def.client.getPhantom(id, phantomId, overrides)`; for a fresh phantom agent, use
  `Def.client.newPhantom(id, overrides)`. Non-secret override leaves are encoded and applied at
  call time; secret overrides are rejected because secrets remain host-provisioned.
- **Cancelable / abortable RPC.** Pass `{ signal }` to an awaited client method, for example
  `await client.run(input, { signal })`. `client.run.schedule(at, input)` returns a
  `CancellationToken` whose `.cancel()` cancels the scheduled invocation. Fire-and-forget remains
  `client.run.trigger(input)`. There are no `.abortable` or `.scheduleCancelable` client methods.
- **`Principal` as data + auto-injected input.** `s.principal()` carries a `Principal` as a method return / nested value, and a bare `s.principal()` method (or constructor) parameter is auto-injected with the per-call caller principal — it consumes no wire field and is not bound from HTTP/RPC callers (the replacement for the decorator's injected `Principal` parameter). `this.getPrincipal()` still reads the init-time principal.

**Known remaining deltas (verify against `src/` before assuming this list is exhaustive):**
- **Custom snapshot is a bare `Uint8Array`.** No `{ data, mimeType }` return, no `application/json` vs `multipart/mixed` selection, and no automatic SQLite multipart part. `save` returns bytes, `load` takes bytes.
- **No mount-level header→id binding.** The mount only binds path `{var}` names to id fields; there is no header-to-id mapping on the mount (an endpoint-level `{ headers }` binding on a method DOES work).

If a decorator feature you need has no current equivalent, stop and flag it rather than inventing an API name — verify against `src/` and `src/host/` first.

## Migration checklist

1. Delete decorator imports and any `@golemcloud/golem-ts-typegen` usage.
2. Remove `experimentalDecorators` / `emitDecoratorMetadata` from `tsconfig.json`.
3. Convert each `@agent` class to `defineAgent({...})` + `.implement({...})`; constructor params → `id`; class fields → `init()` state.
4. Convert method signatures to `method({ input, returns })` with Standard Schema; `@description`/`@prompt`/`@readonly`/`@endpoint` → method options + `http.mount`.
5. Convert `Result<T,E>` returns to `s.result(ok, err)`.
6. Convert `Config`/`Secret` constructor params to a `config` record (`s.secret(...)` for secrets).
7. Convert `save/loadSnapshot` overrides to `snapshotting` (or `.implement({ snapshot })`).
8. Ensure every agent module is imported from `src/main.ts`.
9. Remove the old decorator/base/typegen dependencies and all compatibility wrappers.
10. Build the local SDK bundle and agent template, then point the application build at those local
    packages before verifying:

```shell
cd /path/to/golem/sdks/ts
npx pnpm install
npx pnpm run build
npx pnpm run build-agent-template
cd /path/to/migrated-application
GOLEM_TS_PACKAGES_PATH=/path/to/golem/sdks/ts/packages golem build --yes
```

