| name | migrate-ts-decorator-sdk |
| description | 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. |
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 the SDK source in sdks/ts/packages/golem-ts-sdk/src/ and src/host/ as you go; public APIs are exported from src/index.ts.
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 classes, no decorators, and no code generation. Handlers are plain functions whose this is the state.
Imports
The decorator exports are gone. Remove them and the typegen package:
import { BaseAgent, agent, prompt, description, endpoint, readonly, Config, Secret, Result } from '@golemcloud/golem-ts-sdk';
import { z } from 'zod';
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",
"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() },
http: http.mount('/counters/{name}'),
methods: {
increment: method({
input: {},
returns: z.number(),
description: 'Increases the count by one and returns the new value',
promptHint: 'Increase the count by one',
http: http.post('/increment'),
}),
},
});
export const CounterImpl = Counter.implement({
init: ({ id }) => ({ name: id.name, value: 0 }),
: {
() { . += ; .; },
},
});
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:
divide: method({ input: { a: z.number(), b: z.number() }, returns: s.result(z.number(), z.string()) }),
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(),
apiKey: s.secret(z.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); },
},
});
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:
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: () => new Uint8Array(),
load: (bytes) => { },
},
});
Feature parity — restored capabilities + the remaining deltas
The current API covers the decorator features it once lacked. Use the forms below; only two genuine deltas remain.
Restored to base parity (use these — they are NOT gaps):
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) → clientFor(Def)(id, phantomId?, overrides) — the non-secret override leaves are encoded and applied to the remote agent at call time (secret overrides are rejected; secrets stay host-provisioned).
- Cancelable / abortable RPC. The client method has
.abortable(signal, input) (cancels the remote invocation when the AbortSignal fires) and .scheduleCancelable(at, input) → a CancellationToken (.cancel()), alongside .trigger(input) / .schedule(at, input).
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.
Remaining deltas (verify against src/ before assuming an option exists):
- 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
- Delete decorator imports and any
@golemcloud/golem-ts-typegen usage.
- Remove
experimentalDecorators / emitDecoratorMetadata from tsconfig.json.
- Convert each
@agent class to defineAgent({...}) + .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 to s.result(ok, err).
- Convert
Config/Secret constructor params to a config record (s.secret(...) for secrets).
- Convert
save/loadSnapshot overrides to snapshotting (or .implement({ snapshot })).
- Ensure every agent module is imported from
src/main.ts.
golem build --yes to verify.