# Golem TypeScript SDK

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

const counter = defineAgent({
  name: 'Counter',
  id: { name: z.string() },
  methods: {
    increment: method({ input: {}, returns: z.number() }),
  },
});

export const Counter = counter.implement({
  init() {
    return { value: 0 };
  },
  methods: {
    async increment() {
      return ++this.value;
    },
  },
});
```

The state returned by `init()` or a snapshot `load()` must not contain top-level fields named
`config`, `getId`, `getPhantomId`, or `getPrincipal`. These names are reserved for the SDK's
configuration accessor and identity/principal helpers. The SDK rejects these fields during
initialization or restoration, even if their value is `undefined`. Rename them or nest them
inside another state field; nested fields may use these names.

## HTTP routers and files

`defineHttpRouter` registers a parameterless, ephemeral, snapshot-disabled HTTP router alongside
`defineAgent`. Routers have no ordinary agent client or `dependencies` option. The mount is a
literal public path; authentication and CORS use the ordinary host policies.

```ts
import { defineHttpRouter, withRawHeaders } from '@golemcloud/golem-ts-sdk';
import { z } from 'zod';

export const Web = defineHttpRouter('Web', { config: { greeting: z.string() } })
  .mount('/web', { cors: ['https://example.com'] })
  .static('/assets/*', '/assets/$1')
  .openApi(
    ({ config }) => ({
      openapi: '3.1.0',
      info: { title: config.greeting, version: '1' },
      paths: { '/echo': { post: { responses: { '200': { description: 'Echo' } } } } },
    }),
    { methodName: 'describe' },
  )
  .implement(
    (request, context) => {
      // request.body is an incremental ReadableStream<Uint8Array>, not a buffered payload.
      // context.rawRequest preserves original method, target, query, and byte-valued headers.
      return withRawHeaders(new Response(request.body), [
        { name: 'set-cookie', value: new TextEncoder().encode('a=first') },
        { name: 'set-cookie', value: new TextEncoder().encode('a=second') },
      ]);
    },
    { methodName: 'serve' },
  );

export const Assets = defineHttpRouter('Assets')
  .mount('/assets', {
    fileResponseHeaders: { 'content-security-policy': "default-src 'self'" },
  })
  .static('/*', '/assets/$1')
  .implement();
export const Docs = defineHttpRouter('Docs')
  .mount('/docs')
  .openApi(() => ({
    openapi: '3.1.0',
    info: { title: 'Docs', version: '1' },
    paths: {},
  }))
  .implement();
```

The two method names are arbitrary and must differ. Omitting the handler registers a static-only,
provider-only, or empty router without a dummy method. OpenAPI providers run lazily, not during
registration. Supply files and configuration through the normal application manifest and list
the router in the HTTP API deployment's `agents` map. Static mappings read immutable deployed
files; they do not expose files written by an invocation.
Set `fileResponseHeaders` on either an agent mount or router mount to add those headers when the
host serves exposed or static files.

Web `Headers` normalizes names and combines repeated values; `Request` normalizes URLs and some
standard methods and forbids bodies on GET/HEAD. `context.rawRequest` exposes the original head
without a second body reader. `withRawHeaders` replaces the entire canonical response header
sequence, including independent cookies and non-UTF-8 bytes. Use lowercase field names and
`Uint8Array` values. For methods, targets, statuses or bodies that Web APIs cannot represent, use
the canonical API instead:

```ts
import { AgentStream, defineHttpRouter } from '@golemcloud/golem-ts-sdk';

export const Raw = defineHttpRouter('Raw')
  .mount('/raw')
  .implementRaw((request) => ({
    status: 200,
    headers: [{ name: 'x-byte', value: new Uint8Array([255]) }],
    body: request.body, // AgentStream<Uint8Array>; extension method spelling is unchanged.
  }));
```

The adapter owns input/output disposal. It retains input while the response may consume it and
releases both on response EOF, error, or local disposal. HEAD and 204/205/304 responses dispose
their producers before the native stream pump can poll them. Use `highWaterMark: 0` if a Web
producer must not prefetch. Do not detach work that outlives the returned stream. The host owns
framing, head commitment and session completion; body EOF alone is not session completion.

**Cancellation limitation:** local stream disposal is tested, but host cancellation currently
traps Wasm rather than providing a cooperative JavaScript invocation signal. The pinned native
stream pump can also wait in `next()` before observing a remote reader drop. JavaScript `finally`
execution and prompt disposal during an indefinitely pending pull are not guaranteed on those
paths. `Request.signal` does not represent host invocation cancellation.

Ordinary durable, non-phantom agents expose live files with `exposeFiles`. Every constructor
parameter must be a path-compatible scalar bound exactly once in the mount; the constructor can
create the file before the host reads it. Ordinary methods and clients remain available.

```ts
import { defineAgent, http } from '@golemcloud/golem-ts-sdk';
import { z } from 'zod';
import * as fs from 'node:fs';

export const Files = defineAgent({
  name: 'Files',
  id: { name: z.string() },
  methods: {},
  http: http.mount('/files/{name}', {
    exposeFiles: [{ route: '/report', path: '/report.txt' }],
  }),
}).implement({
  init({ id }) {
    fs.writeFileSync('/report.txt', `Report for ${id.name}`);
    return {};
  },
  methods: {},
});
```

Mappings preserve declaration order. Exact routes target absolute files; a terminal `/*` maps
to a target ending in `/$1`. Public segments are decoded once; filesystem targets are not URI
decoded. Identical compiled pairs are rejected, while the same route with distinct targets is
allowed. The host applies static/live-file selection and path-security rules.

### Public HTTP router entry

Import `defineHttpRouter`, `nodeHttpHandler`, and the following contract helpers from
`@golemcloud/golem-ts-sdk/http-router`. This is a component-runtime entry, not a host-free
Node module. Build applications with the SDK component plugin so router registration and
the HTTP facade share the component's module graph. `nodeHttpHandler` accepts only servers
constructed by the component facade, not native socket-backed Node servers. The facade
does not change the HTTP module used by host-side build tooling.

### Socket-free Node HTTP servers

Use the familiar request listener API, then register it explicitly as a router:

```ts
import { createServer } from 'node:http';
import { defineHttpRouter, nodeHttpHandler } from '@golemcloud/golem-ts-sdk/http-router';

const server = createServer((req, res) => {
  res.writeHead(200, { 'content-type': 'text/plain' });
  res.end('Hello from Golem');
});

export const router = defineHttpRouter('Web').mount('/').implementRaw(nodeHttpHandler(server));
```

List `Web` in the HTTP API deployment's `agents` map in `golem.yaml`, just as for other
routers. The name identifies the deployed router; the mount defines its public prefix.
Use `.mount('/web', { auth: true, cors: ['https://example.com'] })` to apply the normal
host authentication and CORS policies. No `listen()` call or component port is needed.

Alternatively, opt into initialization-time `listen()` registration with a component
`nodeHttpRouters` mapping in `golem.yaml`:

```yaml
components:
  my-app:main:
    templates: ts
    nodeHttpRouters:
      '3000':
        name: Web
        mount: /web
        auth: true
        cors: [https://example.com]
```

Then call `server.listen(3000, callback)` while the application module initializes.
The build injects this configuration before application and bundled dependency evaluation,
in both discovery and execution instances. Still list `Web` in the HTTP API deployment.
Ports are lookup keys, not sockets. `listening` and the asynchronous `listening` event/callback
report successful local router registration, not deployment readiness. `address()` returns
`null`; `close()` throws because it cannot undeploy a route. Invocation-time registration,
duplicate ports or names, and mixing explicit and automatic registration of one server are
rejected. Only a numeric port from 1 through 65535 and an optional callback are accepted.
Templates and presets can also provide the mapping; each supplied table replaces the
inherited table, and `{}` clears it. Explicit registration remains independently usable.

Component builds resolve named and default imports from both `http` and `node:http` to
one facade, including statically bundled CommonJS dependency imports. Runtime-computed
module loading is not supported. Outgoing `request()`, `get()`, client `IncomingMessage`,
and the other original client exports remain the runtime's HTTP implementation.

The server supports `createServer()` or `createServer(listener)` and later
`server.on('request', listener)` registration. Constructor options, custom message classes,
connection management and upgrades are not supported. It is a
local EventEmitter listener container, not a persistent process-wide server: each public
request runs in an ephemeral router instance. Closing a local stream cannot undeploy a route.
This subset does not imply compatibility with every Node web framework.

Request objects inherit the original `IncomingMessage` and support readable-stream operations
such as `on('data')`, async iteration, `pipe()`, `pause()`, `resume()`, and `destroy()`.
Response objects use the runtime's `Writable` with `statusCode`, `setHeader()`, `getHeader()`,
`getHeaders()`, `getHeaderNames()`, `hasHeader()`, `removeHeader()`, `writeHead()` (header object
or raw name/value pairs), `headersSent`, `flushHeaders()`, `write()`, `end()`, and `destroy()`.
Use writable `finish`, `drain`, `error`, and `close` events for local stream lifecycle;
`finish` is not confirmation that the remote caller received all bytes.

Incoming Node requests preserve the method, mount prefix, and original query, including
absent versus empty queries. Body reads are demand-driven and produce `Buffer` values.
Header values use Latin-1 byte strings. `headers` uses lowercase names, keeps the first
singleton header, joins cookies with `; ` and other duplicates with `, `, and retains
`set-cookie` as an array. `headersDistinct` retains all occurrences. `rawHeaders` contains
the occurrences in Golem's canonical envelope plus `host` from its authority; it does not
recover original wire casing or order. The request has no HTTP version, remote address,
or operational socket.

Responses commit their status and headers at `writeHead()`, `flushHeaders()`, the first
`write()`, or `end()`. The handler returns at commitment while the body remains live.
Writable callbacks and `drain` provide cooperative backpressure; callers must pause when
`write()` returns `false`. Ignoring that result can grow the writable queue without bound.
The host, not the adapter, supplies HTTP framing. Repeated `set-cookie` values remain
separate occurrences. Custom reason phrases, trailers, informational responses, upgrades,
and listening sockets are unsupported.

Adapter-observed errors, destruction, and output disposal release both sides of the
exchange. Normal HEAD/204/205/304 and Content-Length-zero suppression discards body writes
without waiting for `end()` or a first body read. Other early disposal cancels local writes.
Request listeners follow EventEmitter semantics: return values are ignored, and rejected
promises from async listeners are not automatically caught. Handle them explicitly.
Remote disconnects use Golem's existing host cancellation and interruption. There is no
handler-context signal that guarantees graceful JavaScript `aborted`/`close` events.
Adapter-created input sources must cooperate with disposal; an arbitrary pending source
promise is not forcibly interrupted.

- `HttpRequest<Body>` / `HttpResponse<Body>` impose no constraint on `Body`. `HttpHeader.value`
  is `Uint8Array`; header arrays are readonly ordered occurrences. Request `query` is
  `string | undefined`: `undefined` means absent; `''` means a present empty query.
- `compileFileMappings(readonly { route: string; path: string }[])` returns WIT-shaped mappings.
- `compileRouterMount({ mount, auth?, cors?, staticBindings?, handlerMethod?, openapiProviderMethod? })`
  accepts already compiled mappings and returns `{ mount, handlerBinding }`. An absent handler
  produces no binding. The helper never rebuilds an adapter's schemas or configuration.
- `copyHttpHeaders` validates/copies the canonical sequence without normalizing occurrences.
- `serializeOpenApi(document: unknown): string` synchronously emits bounded, canonical JSON
  (sorted Unicode keys, preserved array order). It rejects non-JSON values, cycles, unsupported
  root sections, non-3.1.0 documents, depth over 64 and UTF-8 output over 1 MiB. It does not fetch,
  register, evaluate providers, strip fields, rebase paths, or merge domains. The host performs
  full OpenAPI semantic validation, reference handling, rebasing and merging.
- `HttpRouterError.category` provides a safe diagnostic category without document contents.

The TypeScript and Effect SDKs build these primitives from the private `sdks/http-contract`
source package independently. Both bundle its code and declarations; npm consumers need no
private package or sibling SDK checkout. After building the SDK and its template, run
`pnpm --filter @golemcloud/golem-ts-sdk run check:package` from `sdks/ts` to verify a packed
installation, component HTTP imports, and consumer declarations.

The SDK tests execute shared corpus metadata/mapping cases and targeted stream/codec cases;
corpus integrity alone does not prove runtime conformance. The CLI deployed fixture
`typescript_http_router` exercises static files, live files and ordinary invocation, OpenAPI,
extension methods, empty queries, HEAD, early responses, cookie order, and delayed duplex input.

## Unstructured outputs and HTTP responses

`s.unstructuredText()` and `s.unstructuredBinary()` declare unstructured values. A method returns
them as `{ tag: 'inline', val, ... }` with the data or `{ tag: 'url', val }`. Wrap them in
`s.option(inner)` for an optional value (`undefined` is `none`) and in `s.result(ok, err)` for a
typed error:

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

const Pending = z.object({ tag: z.literal('pending') });

export const Images = defineAgent({
  name: 'Images',
  id: { bucket: z.string() },
  http: http.mount('/images/{bucket}'),
  methods: {
    download: method({
      input: { id: z.string() },
      returns: s.result(s.option(s.unstructuredBinary({ mimeTypes: ['image/png'] })), Pending),
      http: http.get('/{id}'),
    }),
  },
}).implement({
  init: () => ({}),
  methods: {
    download: ({ id }) =>
      id === 'missing'
        ? Result.ok(undefined)
        : id === 'pending'
          ? Result.err({ tag: 'pending' as const })
          : Result.ok({ tag: 'inline' as const, val: new Uint8Array(), mimeType: 'image/png' }),
  },
});
```

The output schema keeps the `unstructured-text` or `unstructured-binary` role on the nested
`variant { inline, url }`. The Golem HTTP host maps the output of an HTTP endpoint method as
follows:

| Output                       | Response                                                        |
| ---------------------------- | --------------------------------------------------------------- |
| element                      | `inline` → 200 with the raw body; `url` → redirect              |
| `option<element>`            | `none` → 404; `some` → as the element                           |
| `result<element, E>`         | `ok` → as the element; `err` → 500 with the error as JSON       |
| `result<option<element>, E>` | `ok(none)` → 404; `ok(some)` → as the element; `err` → 500 JSON |

Other return schemas use the same `option` and `result` rules with a JSON body in place of the raw
body.

## Streaming methods

Use `s.stream(itemSchema)` in a method schema and `AgentStream<T>` in its implementation. Streams
can appear at the root or nested inside records, tuples, options, results, variants, and lists.

```ts
import { AgentStream, defineAgent, method, s } from '@golemcloud/golem-ts-sdk';

const numbers = defineAgent({
  name: 'Numbers',
  id: {},
  methods: {
    doubled: method({
      input: { values: s.stream(s.u32()) },
      returns: s.stream(s.u32()),
    }),
  },
});

export const Numbers = numbers.implement({
  init() {
    return {};
  },
  methods: {
    doubled({ values }) {
      return AgentStream.from(
        (async function* () {
          for await (const value of values) yield value * 2;
        })(),
      );
    },
  },
});
```

`AgentStream` is lazy and single-reader, so await each operation before starting another. Clean EOF
is `{ done: true, value: undefined }`. Encoding or forwarding a stream transfers its ownership; do
not reuse the original object.

For a stream received through a connected P3 agent invocation, early exit from `for await` calls
`return()` and closes the readable endpoint. `throw(reason)` also closes it and rejects locally with
`reason`, which is not sent to the producer. When an `AgentStream.from` source is sent through P3,
accepted writes gate later pulls, providing back-pressure. When a subsequent write observes a
remote reader drop, production stops and awaits the source iterator's `return()` exactly once. P3
does not interrupt an arbitrary pending source `next()` or guarantee cleanup before a later agent
invocation. Producer and cleanup failures fail the active operation or invocation session rather
than becoming clean EOF. P3 has no recoverable stream-local terminal error, so model one explicitly
in the item type, for example `stream<result<T, E>>`, when needed.

## Agent client approaches and reflection

Choose the narrowest caller surface that owns the information you have:

1. Normal RPC uses a shared source definition through the SDK's ordinary typed client and owns all lifecycle factories allowed by its mode.
2. Caller-defined static clients use local schemas without discovery. `defineAgentClient({ methods })` binds an existing durable canonical or phantom `ParsedAgentId`; `defineAgentClient({ name, id, methods, mode?, config? })` adds identity construction, lifecycle factories, and declaration-aware config validation.
3. Discovered clients use `reflection.getReflectedAgentType` and retain an immutable deployed metadata snapshot for runtime-selected types and methods.
4. Fully dynamic clients use `ParsedAgentId.dynamicClient()` to transport schema-native values on an existing durable identity. They do not retain schemas or create a target.

Normal RPC and caller-defined static clients encode through their local schemas before opening RPC. Full-client binding also checks the declared type name and constructor shape. Discovered clients apply the complete snapshot input restrictions and validate output cardinality and shape. The host remains authoritative for visibility, authorization, effective configuration, identity resolution, and deployed-schema validation.

Typed inputs use the schema library's normal optional-field syntax. Canonical reflected JSON records contain every field; use `null` for an absent `option<T>`. `s64` and `u64` are canonical decimal strings, duration is `{ nanoseconds: "..." }`, and quantity uses a decimal-string `mantissa`; smaller integers remain numbers. Schema-native streams and opaque capabilities require the `*Value` APIs and transfer ownership once.

```ts
import { getReflectedAgentType, isRemoteCallError } from '@golemcloud/golem-ts-sdk';

const type = getReflectedAgentType('Counter');
if (!type || type.mode !== 'durable') throw new Error('Counter is unavailable');
const client = type.client.get({ name: 'main' });
const add = client.method('add');

try {
  const abort = new AbortController();
  const result = await add.invoke({ by: 1, note: null }, abort.signal);
  console.log(result.value, result.metadata.agentId);
  const scheduled = add.schedule({ seconds: 1n, nanoseconds: 0 }, { by: 2, note: null });
  scheduled.cancellationToken.cancel();
} catch (error) {
  if (isRemoteCallError(error)) console.error(error.cause);
  else throw error;
}
```

## Runtime tool reflection

Use `reflection.getToolType(name)` or `reflection.getAllToolTypes()` to discover tools visible to
the calling component. A `ToolType` is an immutable metadata snapshot. Resolve command names or
aliases with `tool.client.command(path)`; the returned command exposes its canonical `path`,
ordered arguments, input schema, result schema, and child commands. Namespace commands appear in
the tree but cannot be invoked until a command body is selected.

```ts
import { reflection } from '@golemcloud/golem-ts-sdk';

const tool = reflection.getToolType('weather');
if (tool) {
  const forecast = tool.client.command(['forecast']);
  const result = await forecast.invokeJson({ city: 'Budapest' });
}
```

Canonical JSON records include every argument key; use `null` for an absent optional value.
`invokeValue` accepts a schema-native value. Both forms validate against the discovered schema
before opening RPC and check declared results after invocation. `startJson` and `startValue`
expose independent stdout and stderr streams, result, `collect`, and cancellation for pending
calls; use them when either output is required. Drain every declared output concurrently with
observing the result, or use `collect()` to settle all three. Result failures take precedence over
output failures. Consume or cancel every started call. `DynamicToolClient` accepts a caller-packed typed schema value without pretending to
know the deployed schema. Reflected output failures reject with `ToolRemoteOutputError`.

## Retrying user code

`retry` interprets the same semantic policies used by Golem in ordinary user space. The callback may
return synchronously or return a Promise. Use `properties` when a filtered policy needs values from
the current failure, and use an `AbortSignal` to cancel an attempt or delay.

```ts
import { Duration, Policy, Predicate, retry } from '@golemcloud/golem-ts-sdk';

const policy = Policy.exponential(Duration.milliseconds(100), 2)
  .maxRetries(4)
  .onlyWhen(Predicate.eq('transient', true));

const response = await retry(policy, () => callUnreliableService(), {
  properties: (error) => ({ transient: error instanceof TransientError }),
  signal,
});
```

Aborting stops waiting for an in-flight callback but cannot stop arbitrary user code. Pass the same
signal into the underlying operation when it supports cancellation.

Local `prop-matches` predicates translate `*` to JavaScript regex `.*` and `?` to `.`, without
flags; all other characters are literal. Wildcards exclude line terminators, and `?` matches one
UTF-16 code unit. This does not implement the host's full glob syntax. Use host-driven retries when
authoritative platform matching is required.

These are local function calls, not executor-managed retry attempts. They do not create executor
retry-attempt oplog entries or survive suspension and recovery as one host-managed retry sequence.

## External Durable Streams

The SDK reads and appends existing external Durable Streams through reader and writer resources
in `golem:agent/durable-streams@2.0.0`. Each SDK factory synchronously constructs one journaled
resource, capturing its immutable URL, mode or producer identity, timeout and optional secret
identity without HTTP. Subsequent asynchronous reads record only checkpoint, transport and
content type; appends record only payload, sequence and close flag. Retries reuse that resource.
The SDK does not create remote streams or send HTTP itself. Custom component worlds using these
APIs must import that interface.

```ts
import {
  readDurableJsonStream,
  readDurableByteStream,
  createDurableJsonWriter,
  createDurableByteWriter,
  s,
} from '@golemcloud/golem-ts-sdk';

const url = 'https://streams.example/events';
const events = readDurableJsonStream(s.u32(), { url, offset: '-1', live: 'sse' });
for await (const event of events) {
  if (event === 42) break;
}

const writer = createDurableJsonWriter(s.u32(), { url, producerId: 'my-producer' });
await writer.append([11, 23]);
await writer.append([47], { close: true }); // one atomic append-and-close

const bytes = readDurableByteStream({ url: 'https://streams.example/bytes' });
const byteWriter = createDurableByteWriter({ url: 'https://streams.example/other-bytes' });
await byteWriter.append(new Uint8Array([3, 241, 27]));
await byteWriter.close(); // close-only consumes a producer sequence too
```

Readers return ordinary `AgentStream<T>` values. Byte readers yield individual `number` bytes for
`s.stream(s.u8())`; external append boundaries are not message boundaries. They can be forwarded
as native method inputs or outputs, including nested streams, with the ownership, failure and
backpressure rules above. A reader retains one complete batch and an index; it drains that batch
before requesting the next opaque offset/cursor. Final data is delivered before closed EOF. Empty
responses and up-to-date markers are not closure. `offset: 'now'` is resolved once by catch-up;
subsequent reads use that concrete checkpoint and the original content type, including for SSE.
EOF and `return()` release the reader resource, including when forwarded through a native stream.
Use `return()` to release an unused reader or one abandoned after a local decode or host failure.

JSON uses the SDK's canonical schema representation by default. The canonical codec rejects
64-bit integers outside JavaScript's safe-number range rather than rounding them. For an external
representation needing exact application conversion, provide deterministic `decode`/`encode`
callbacks; the SDK still checks the application value against the compiled schema. `decode`
receives each original complete JSON value as text, with numeric lexemes preserved. `encode`
returns one complete value; the host supplies the outer batch array. An array-valued message
therefore stays one message, rather than being flattened. For example, an unquoted u64 stream:

```ts
const integers = readDurableJsonStream(s.u64(), { url, decode: (text) => BigInt(text) });
const integerWriter = createDurableJsonWriter(s.u64(), {
  url,
  encode: (value) => value.toString(),
});
await integerWriter.append([18446744073709551615n]);
```

The default attempt timeout is 30000 ms (allowed: 1–300000). Each batch operation automatically
retries timeout, transport, rate-limited and unavailable failures at most five times, using the
runtime's durable timers, exponential backoff starting at 100 ms and capped at 30000 ms, and never
waiting less than `Retry-After`. `maxRetries` and `retryDelayMs` configure this policy. Reconstruction
replays failed attempts and waits; it does not restart the retry budget. An explicit later operation
or `retryPending()` starts a new bounded resolution attempt. Empty live responses wait
`idleDelayMs` (default 100 ms) before another pull. Active HTTP awaits remain resident until their
bounded deadline; only idle durable timer waits can unload. Do not enclose these waits in custom
durability scopes.

Client deadline expiry counts against the failure budget even for a quiet live stream. For
long-poll, set `timeoutMs` above the server's polling window. SSE peers that send no completed
control event before the deadline may exhaust the retry budget during prolonged silence. The SDK
does not turn timeouts into fabricated successful checkpoints. `Retry-After` can exceed the
exponential backoff cap; the SDK honors it without imposing a total elapsed-time limit.

Writers serialize operations per instance, while independent writers can overlap. They copy and
retain one assigned producer tuple, exact encoded payload and close flag before the host append.
Any host failure or cancellation retains this request, including non-retryable failures such as
`payload-too-large`. `retryPending()` resolves it without new data; `append` and `close` also resolve
it before assigning a new sequence. This prevents replacing a body that may have committed during
an earlier uncertain attempt. A duplicate acknowledgement advances exactly once. Acknowledged
sequences greater than the submitted sequence raise
`DurableStreamError` with kind `producer-diverged`. The SDK never steals epochs, renumbers pending
data, or splits oversized requests. Epoch and sequence are bounded by 2^53-1; an explicit new epoch
starts at sequence zero. Empty appends require `close: true`.
An acknowledged closed receipt releases the writer resource. `await writer.dispose()` releases
it without closing the remote stream, after resolving any pending request; if resolution fails,
the handle and pending data remain available for another attempt. Disposal is serialized with
appends and is idempotent. Appending after disposal fails with `closed`.

Append receipts contain `epoch`, `sequence`, `closed`, and an optional `nextOffset`. A duplicate
acknowledgement may omit `Stream-Next-Offset`; the SDK preserves that absence as `undefined`, not
an empty string. There is no `duplicate` flag: HTTP 204 can acknowledge either a duplicate append
or a fresh close-only operation. Producer progress advances from the acknowledged tuple, regardless
of whether an offset was supplied.

`DurableStreamError.kind` preserves the host's typed failure classification and exposes
`retryAfterMs`, `producerEpoch` and `expectedSequence` when supplied. Missing streams (`not-found`),
retention loss/deletion (`gone`), fencing, conflicts and oversized payloads are errors, not EOF.
The host enforces its configured batch-size limit; retrying an oversized read with the same limit
cannot make progress. Local decode failures do not consume the failed item. Forwarded failures fail
the active producer/session; use an explicitly result-valued stream for recoverable application
errors. Closing a reader prevents new pulls, but the existing native stream API cannot interrupt an
arbitrary pending async import; deadlines bound that interval.

Pass a borrowed raw secret capability (for example an `s.secret(s.string())` method argument) as
`auth`. It goes directly to the host; do not call `Secret.get()` or reveal a bearer token in the SDK.
The configuration `Secret<T>` wrapper is not a raw capability and cannot be passed as `auth`.
The host enforces reveal/network permissions and pins the secret revision. HTTPS is required except
for localhost/literal loopback development URLs; URL credentials and redirects are rejected.

Durability uses ordinary Golem host-call replay, without a custom wrapper. Completed replay
performs no external HTTP. Interrupted writes retain the platform's fail-closed recovery behavior. Reader/writer
buffers reconstruct from the oplog and deterministic execution; no snapshot export is provided,
and existing restrictions on snapshots of active native streams remain.

A copied-oplog Golem fork/revert retains the **same external URL, producer ID, epoch and checkpoint**.
It does not create an external stream fork or independent producer. Divergent branches may reuse
the same tuple; the server may deduplicate without comparing bodies. Exactly-once effects depend
on the peer atomically persisting data and producer state and retaining deduplication records.
Construct a writer with a different ID, or an explicitly higher epoch and sequence zero, only when
that is the intended external identity; never discard uncertain data to do so.

The SDK uses Standard Schema-compatible schemas to define agent identities,
method inputs, and method results.

From `sdks/ts`, run `pnpm build` to build all TypeScript packages and
`pnpm test` to run their tests.

## Tool middleware

Typed middleware declares installation parameters with `parameterSchema`; the decoded static type is available as `context.parameters`. Omit it for the normalized empty-record `{}` schema. Universal middleware uses the same option. The compile-backed forms are exercised in `tests/tool.test-d.ts` and `tests/tool-middleware.test.ts`.

Typed underlying methods retain the convenient awaited call and add `.start(args)`. A started call exposes an independent `result` promise, optional `stdout` and `stderr`, and `cancel()`, so calls may overlap and their results and streams may be consumed in any order. Universal middleware uses `await underlying.invoke(...)` for the same started shape, or `underlying.invokeAndAwait(...)` for the convenience form.

The underlying is revoked when the middleware handler returns: no new calls may start, but already admitted calls are not implicitly cancelled. Releasing an invocation result observer is disposal, not cancellation; call `cancel()` explicitly when intended. The SDK disposes abandoned observers and streams.

For commands declaring stdout or stderr, the guest receives the matching host writers. The SDK forwards each middleware-selected output to its writer concurrently with the other output and the result, finishes it on clean EOF, and fails it on forwarding errors. Authors return or select the readable output streams and do not write to or finish the host writers directly.

## Component bundling

All TypeScript components use the same full `agent-guest` WIT world. The component Rollup
configuration infers registration capabilities from the TypeScript program and generates static
imports for agent, tool, middleware, and snapshot exports. Absent capabilities use small empty
discovery/error implementations; no role or alternate world is selected.

The application bundles the SDK's preserved runtime modules, rather than importing a complete
SDK embedded in the wrapper. Rollup can therefore remove absent registries and agent snapshot,
principal serialization, and lifecycle code when they are unreachable. Builder methods are not
rewritten or removed by the build. Opaque registration helpers and computed SDK access conservatively
retain capabilities. Host modules remain external.

Agent, configuration, schema, and tool definitions are evaluated normally at runtime, including
metadata computed by application functions. Component builds do not statically evaluate or rewrite
these definitions.

After building the SDK and agent template, run `node scripts/measure-components.mjs` from this
package to build the five capability fixtures, check their full-world ABI, and record JS,
release/stripped, and preinitialized component sizes in `.component-measurements/sizes.json`.
Pass a baseline full-SDK template WASM as the first argument to compare the old external-SDK
build. The recorded timing is median end-to-end QuickJS preinitialization time (three runs),
including Wasmtime compilation; it is not deployed invocation latency. The harness requires
`wasm-rquickjs` and `wasm-tools` on PATH and is separate from the process-free unit suite.

## Sagas

`infallibleSaga` returns `Result<Out, SagaRetryFailure>`. It compensates returned step failures
and caught exceptions in reverse order, then resolves the `saga` retry policy for the agent URI.
The host receives the same `attempt` and `error-kind` context for selection and evaluation.
It owns the opaque continuation, jitter and finite retry bounds; the SDK does not interpret
the policy tree. No selected policy uses `config.retry`. Policy changes are errors, not a new
retry budget. Exhaustion and policy errors return a typed failure rather than trapping.

Elapsed time includes work, compensation and durable timer waits from before the first attempt.
Attempts append fresh calls; they do not rewind local state. Compensate local changes explicitly
and do not reuse a compensated external operation's idempotency key.

Partial compensation returns a failure with the original error and compensation failure, without
retrying. `fallibleSaga` returns complete or partial rollback metadata for returned failures.
Caught exceptions run compensation and are rethrown unchanged after complete rollback.
Uncatchable guest traps cannot run guest compensation.
