Migrating TypeScript Agents from the Decorator SDK
SkillAI & modelsMigrating 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Migrating TypeScript Agents from the Decorator SDK skill
What this skill tells your AI
The instructions your AI receives, as published by golemcloud/golem in .agents/skills/migrate-ts-decorator-sdk/SKILL.md and read by ahel’s review.
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:
// 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, clientFor, 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:
{
"compilerOptions": {
"moduleResolution": "bundler", // keep
// "experimentalDecorators": true, // REMOVE
// "emitDecoratorMetadata": true, // REMOVE
"strict": true,
"useDefineForClassFields": false
}
}
Agent class → defineAgent + implement
OLD:
@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:
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 viadefineAgent({ 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:
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 / Secret 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):
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:
// 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:
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:
readOnlycache policies.@readonly({ cache: 'no-cache' | 'until-write' | { ttl } })→method({ readOnly: { cache: 'no-cache' | 'until-write' | { ttlNanos: <bigint> }, usesPrincipal?: boolean } }). BarereadOnly: trueuses theuntil-writepolicy (the base default); principal-dependent caching →usesPrincipal: true.- Config-on-RPC (
getWithConfig).Agent.getWithConfig(id, overrides)→clientFor(Def)(id, undefined, overrides). For a fresh phantom agent, useclientFor(Def).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 exampleawait client.run(input, { signal }).client.run.schedule(at, input)returns aCancellationTokenwhose.cancel()cancels the scheduled invocation. Fire-and-forget remainsclient.run.trigger(input). There are no.abortableor.scheduleCancelableclient methods. Principalas data + auto-injected input.s.principal()carries aPrincipalas a method return / nested value, and a bares.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 injectedPrincipalparameter).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, noapplication/jsonvsmultipart/mixedselection, and no automatic SQLite multipart part.savereturns bytes,loadtakes 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
- Delete decorator imports and any
@golemcloud/golem-ts-typegenusage. - Remove
experimentalDecorators/emitDecoratorMetadatafromtsconfig.json. - Convert each
@agentclass todefineAgent({...})+.implement({...}); constructor params →id; class fields →init()state. - Convert method signatures to
method({ input, returns })with Standard Schema;@description/@prompt/@readonly/@endpoint→ method options +http.mount. - Convert
Result<T,E>returns tos.result(ok, err). - Convert
Config/Secretconstructor params to aconfigrecord (s.secret(...)for secrets). - Convert
save/loadSnapshotoverrides tosnapshotting(or.implement({ snapshot })). - Ensure every agent module is imported from
src/main.ts. - Remove the old decorator/base/typegen dependencies and all compatibility wrappers.
- Build the local SDK bundle and agent template, then point the application build at those local packages before verifying:
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
Signals
- GitHub stars
- 2k
- Forks
- 212
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
migrate-ts-decorator-sdk- Source
- github.com/golemcloud/golem