Titentiten.devꦠꦶꦠꦺꦤ꧀
v0.10.0 · stableGitHub
DocsIntegrateAgent SDK

Agent SDK

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

titen-memory/sdk25 operationsno required depsNode 22+ · Bun · Deno · edge

Install

bun add titen-memory
npm i titen-memory
pnpm add titen-memory

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

agent.tstypescript
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
});

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:

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 25 operations

Health Route
health() GET /healthz
ready() GET /readyz
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
Claim lifecycle Route
supersede(claimId, supersededBy, expectedVersion, reason?) POST /v1/claims/:id/supersede
revoke(claimId, expectedVersion, reason?) POST /v1/claims/:id/revoke
expire(claimId, expectedVersion, reason?) POST /v1/claims/:id/expire
Checkpoints 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
Coordination and views Route
acquireLease(options) POST /v1/leases
releaseLease(leaseId) DELETE /v1/leases/:id
createHandoff(options) POST /v1/handoffs
listHandoffs(status?) GET /v1/handoffs
resolveHandoff(handoffId, status) POST /v1/handoffs/:id/resolve
compileView(lens, options?) POST /v1/memory-views/compile
Keys and events Route
createKey(options) POST /v1/keys
listKeys() GET /v1/keys
revokeKey(keyId) DELETE /v1/keys/:id
listEvents(options?) GET /v1/events
iterateEvents(options?) Safely page GET /v1/events until empty

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.

guard.tstypescript
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.'
  }
}

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.

NO AUTOMATIC RETRIES

The client does not retry on its own. Mutation options can send Idempotency-Key on the observation, consolidation and feedback paths; a caller-written retry on any other mutation still follows that route’s normal transition or uniqueness rule.

What the SDK does not cover

No method covers MCP, webhooks, workspaces and memberships, governed policies/approvals, knowledge releases, retention, identity mappings, audit, federation, export/import or the operator drains. Use fetch with the same bearer header for those narrower operator paths.

pull-handoffs.tstypescript
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
↑↓ navigate↵ open