# Agent SDK

> TitenClient is one file of TypeScript over plain fetch, with no required dependencies and one method per route it covers.

Section: Integrate · Source: https://titen.dev/docs/sdk
Derived from: https://github.com/RamaAditya49/titen/blob/main/docs/agent-guide.md

---
## Install

<div class="pkgtabs">
<div data-pkg="bun">

```sh
bun add titen-memory
```

</div>
<div data-pkg="npm">

```sh
npm i titen-memory
```

</div>
<div data-pkg="pnpm">

```sh
pnpm add titen-memory
```

</div>
</div>

`titen-memory` and `titen-memory/sdk` resolve to the same module. Under Bun you get
`src/sdk.ts` directly; everywhere else you get `dist/npm/sdk.js`, which `prepack` builds with
`bun build --target node --format esm`. Engines are Node 22+ and Bun 1.2+, and the package
has no required dependencies.

## Configure

<figure class="code"><figcaption>agent.ts<b>typescript</b></figcaption>

```ts
import { TitenClient } from 'titen-memory/sdk';

const titen = new TitenClient({
  url: 'http://127.0.0.1:8787',     // trailing slashes are stripped
  key: process.env.TITEN_API_KEY!,  // sent as `authorization: Bearer …`
  // fetch: myWrappedFetch,         // optional; defaults to globalThis.fetch
});
```

</figure>

Three fields, and `fetch` is the only seam: no interceptor, retry or pooling layer of its
own. That is why the same client runs unchanged on Node 22+, Bun, Deno and edge workers.
For retries or tracing, wrap `fetch` and pass your wrapper in.

## Every method resolves to `data`

The client reads the `{ data, meta }` envelope and returns `data`, so fields sit at the top
level of what you await:

```ts
const obs = await titen.observe({
  subject_id: 'user_rama',
  kind: 'tool_result',
  content: 'Deploy smoke returned 200 for checkout-service.',
  source: { type: 'tool', ref: 'deploy_789#smoke' },
  trust: 'verified',
});
obs.observation_id; // "obs_56a95360c09c4ee393c53600c4459079"
obs.content_hash;   // "bba5cf65e8aee81f…"
```

`meta` is dropped: `meta.request_id`, `meta.replayed` on an idempotent write, and
`meta.degraded` on a compile, the flag that tells you whether the pack was assembled without
vectors or without a model. When you need any of those, call the route with `fetch` instead.

## The 17 methods

<div class="table-wrap">

| Health | Route |
| --- | --- |
| `health()` | `GET /healthz` |
| `ready()` | `GET /readyz` |

</div>

<div class="table-wrap">

| The memory loop | Route |
| --- | --- |
| `resolveProject(reference: string, create = false)` | `POST /v1/projects/resolve` |
| `observe(observation: Observation)` | `POST /v1/observations` |
| `consolidate(subject_id: string, claims: Claim[], project_id?: string)` | `POST /v1/consolidations` |
| `compile(options: CompileOptions)` | `POST /v1/context/compile` |
| `feedback(contextId: string, options: FeedbackOptions)` | `POST /v1/context/:id/feedback` |
| `evidence(claimId: string)` | `GET /v1/claims/:id/evidence` |

</div>

<div class="table-wrap">

| Claim lifecycle | Route |
| --- | --- |
| `supersede(claimId: string, supersededBy: string, reason?: string)` | `POST /v1/claims/:id/supersede` |
| `revoke(claimId: string, reason?: string)` | `POST /v1/claims/:id/revoke` |
| `expire(claimId: string, reason?: string)` | `POST /v1/claims/:id/expire` |

</div>

<div class="table-wrap">

| Checkpoints and keys | Route |
| --- | --- |
| `saveCheckpoint(options: CheckpointOptions)` | `POST /v1/checkpoints` |
| `getCheckpoint(subject_id: string, kind: string, agent_id?: string)` | `GET /v1/checkpoints` |
| `deleteCheckpoint(checkpointId: string)` | `DELETE /v1/checkpoints/:id` |
| `createKey(options: { label: string; scopes: string[]; max_trust?: string })` | `POST /v1/keys` |
| `listKeys()` | `GET /v1/keys` |
| `revokeKey(keyId: string)` | `DELETE /v1/keys/:id` |

</div>

`Observation`, `Claim`, `CompileOptions`, `FeedbackOptions` and `CheckpointOptions` are all
exported, and they carry the enums as literal unions: `Observation.kind`,
`Observation.trust`, `Observation.visibility`, `Claim.sources[].relation`,
`FeedbackOptions.outcome` and `CheckpointOptions.kind`. A typo in any of those is a
build-time type error, not a `400` at runtime.

## Error handling

Any non-2xx response throws `TitenError`, carrying the status alongside the service's own
error code and message.

<figure class="code"><figcaption>guard.ts<b>typescript</b></figcaption>

```ts
import { TitenClient, TitenError } from 'titen-memory/sdk';

try {
  await titen.observe({ subject_id: '', kind: 'tool_result', content: 'x', source: { type: 't' } });
} catch (err) {
  if (err instanceof TitenError) {
    err.status;  // 400
    err.code;    // "VALIDATION_ERROR"
    err.message; // 'Field "subject_id" must be a non-empty string.'
  }
}
```

</figure>

The three you will meet most: `400 VALIDATION_ERROR` with the offending field named,
`401 UNAUTHENTICATED` for a missing or revoked key, and `403 FORBIDDEN` for either a missing
scope (`Missing required scope "claims:write".`) or a trust ceiling
(`This credential may not assert "verified" trust.`).

Two edges. `code` falls back to `"UNKNOWN"` and the message to `"Request failed"` when the
body has no error envelope. And the client parses the body *before* it checks the status, so
a response that is not JSON at all (an HTML error page from a proxy in front of the service)
rejects with the parse error rather than a `TitenError`.

<div class="callout">
<strong>NO RETRIES, AND NO IDEMPOTENCY HEADER</strong>

The client sends exactly two headers: `authorization` and, on a body request,
`content-type`. It never sends `Idempotency-Key`, so a retry you write yourself is a second
write. `FeedbackOptions` carries a `client_mutation_id` field for that reason; `Observation`
and `Claim` do not.
</div>

## What the SDK does not cover

No method covers MCP, events and webhooks, workspaces and memberships, leases and handoffs,
policies, channel releases, audit, federation, export and import, memory views or index
drain. The client wraps the whole Level 5 loop plus the credential management around it;
reach the collaboration plane through MCP or through `fetch` with the same bearer header.

<figure class="code"><figcaption>pull-handoffs.ts<b>typescript</b></figcaption>

```ts
const res = await fetch('http://127.0.0.1:8787/v1/handoffs', {
  headers: { authorization: `Bearer ${process.env.TITEN_API_KEY}` },
});
const { data, meta } = await res.json(); // meta.request_id is available here
```

</figure>

<div class="cards">

<a href="/docs/mcp"><strong>MCP server</strong><span>Seven tools over JSON-RPC, for hosts that would rather not import a client.</span></a>
<a href="/docs/keys-scopes"><strong>Keys &amp; scopes</strong><span>The scope list and trust ceilings behind every 403 above.</span></a>
<a href="/docs/api"><strong>HTTP API</strong><span>All 58 routes, for the 41 the client does not wrap.</span></a>

</div>