Adding a New Agent to a TypeScript Golem Component

SkillAI & models

Adding a new TypeScript agent to a Golem component. Use when the user asks to create, add, or define a new agent type, implement an agent class, or add agent methods in a TypeScript Golem project.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Adding a New Agent to a TypeScript Golem Component skill

What this skill tells your AI

The instructions your AI receives, as published by golemcloud/golem in golem-skills/skills/ts/golem-add-agent-ts/SKILL.md and read by ahel’s review.

Overview

An agent is a durable, stateful unit of computation in Golem. Each agent type is declared with defineAgent(...) and given behaviour with .implement(...) from @golemcloud/golem-ts-sdk. Method inputs and return values are described with Standard Schema values (Zod is used throughout these examples; Valibot, ArkType, and Effect Schema also work).

Steps

  1. Create the agent file — add a new file src/<agent-name>.ts
  2. Declare the contract — defineAgent({ name, id, methods })
  3. Supply the behaviour — <Def>.implement({ init, methods })
  4. Import from main.ts — add import './<agent-name>.js'; to src/main.ts
  5. Build — run golem build to verify

src/main.ts is the entrypoint module that must import each agent module for side effects. defineAgent / .implement register the agent at module-load time, so importing the module is sufficient for discovery — no class needs to be exported.

Agent Definition

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

// The contract: `name` is the wire-level agent type, `id` is the identity record
// (its fields form the constructor parameters), and `methods` declares the typed
// method surface.
export const Counter = defineAgent({
    name: 'Counter',
    id: { name: z.string() },
    methods: {
        increment: method({ input: {}, returns: z.number() }),
        getCount: method({ input: {}, returns: z.number() }),
    },
});

// The behaviour: `init` returns the initial state; each handler's `this` is bound
// to that state (plus SDK helpers like `getId()`).
export const CounterImpl = Counter.implement({
    init: () => ({ value: 0 }),
    methods: {
        increment() {
            this.value += 1;
            return this.value;
        },
        getCount() {
            return this.value;
        },
    },
});

State lives in the object returned by init() and is read/written through this inside the handlers — there are no class fields. A method with no parameters declares input: {} and its handler takes no argument; a method with parameters declares one schema per named parameter and receives them as a single destructured object.

Custom Types

Describe parameter and return shapes with schemas. For object types, define a schema once and derive the TypeScript type from it with z.infer. TypeScript enums are not supported — use a string-literal union (z.enum([...])) instead:

const Coordinates = z.object({ lat: z.number(), lon: z.number() });
const WeatherReport = z.object({ temperature: z.number(), description: z.string() });
const Priority = z.enum(['low', 'medium', 'high']);

export const WeatherAgent = defineAgent({
    name: 'WeatherAgent',
    id: { apiKey: z.string() },
    methods: {
        getWeather: method({ input: { coords: Coordinates }, returns: WeatherReport }),
    },
});

export const WeatherAgentImpl = WeatherAgent.implement({
    init: () => ({}),
    methods: {
        getWeather({ coords }) {
            // ...
            return { temperature: 21, description: 'clear' };
        },
    },
});

Returning Failures

Agent methods should distinguish between domain errors (expected failure outcomes) and uncaught errors:

  • Uncaught errors (thrown exceptions, rejected promises) are not returned to the caller as a failed invocation. Golem treats them as crashes: the invocation is retried according to the agent's retry policy, and if the retries are exhausted the agent itself becomes failed.
  • Domain errors that the caller should observe as a normal failure result must be expressed in the method's returns schema as s.result(okSchema, errSchema). The handler then returns Result.ok(v) / Result.err(e), and the caller receives a decoded Result<Ok, Err>.
import { z } from 'zod';
import { defineAgent, method, s, Result } from '@golemcloud/golem-ts-sdk';

const WithdrawError = z.discriminatedUnion('tag', [
    z.object({ tag: z.literal('insufficientFunds'), available: z.number() }),
    z.object({ tag: z.literal('accountClosed') }),
]);

export const Wallet = defineAgent({
    name: 'Wallet',
    id: { owner: z.string() },
    methods: {
        withdraw: method({
            input: { amount: z.number() },
            returns: s.result(z.number(), WithdrawError),
        }),
    },
});

export const WalletImpl = Wallet.implement({
    init: () => ({ balance: 0, closed: false }),
    methods: {
        withdraw({ amount }) {
            if (this.closed) {
                return Result.err({ tag: 'accountClosed' as const });
            }
            if (amount > this.balance) {
                return Result.err({ tag: 'insufficientFunds' as const, available: this.balance });
            }
            this.balance -= amount;
            return Result.ok(this.balance);
        },
    },
});

Returning Result.err(...) completes the invocation successfully — the caller receives the error as a value. Throwing (e.g. throw new Error(...)) or returning a rejected promise will instead trigger a retry and eventually fail the whole agent.

If the throw happens after a durable side effect has already completed, retry replays that side effect's recorded result. A common example is const response = await fetch(...); if (!response.ok) throw ...: the HTTP response status/body are recorded before the exception is thrown. Wrap the fetch, body read, and status check in atomically(...) when the exception should make Golem re-execute the HTTP request instead of replaying the recorded response.

Related Skills

  • Load golem-js-runtime for details on the QuickJS runtime environment, available Web/Node.js APIs, and npm compatibility
  • Load golem-file-io-ts for reading and writing files from agent code

Key Constraints

  • Every agent is declared with defineAgent({...}) and given behaviour with <Def>.implement({...})
  • The id record fields define agent identity — they must be serializable schema types
  • State is the object returned by init() and is accessed through this in the handlers — do not use class fields
  • TypeScript enums are not supported — use z.enum([...]) / string-literal unions instead
  • Agents are created implicitly on first invocation — no separate creation step
  • Invocations are processed sequentially in a single thread — no concurrency within a single agent
  • Each method's input is a record of one schema per named parameter; returns is the success-value schema (use z.void() for no return value)

Signals

GitHub stars
2k
Forks
210
Last commit
Sep 2026
Advanced
Item type
skill
Key
golem-add-agent-ts
Source
github.com/golemcloud/golem