# Letta Agent SDK contributor map

## Canonical source

Work in `src/`. `dist/` is generated by `bun run build`, ignored by Git, and
published as a bundle. A repeated class or method in `dist/index.js` or
`dist/client-entry.js` can represent separate source-layer responsibilities
that Bun placed in one file; trace it back to `src/` before changing it.

## Session architecture

- `client-base.ts` selects the backend and constructs a session.
- Remote, Cloud, and the default local app-server path share
  `remote-client-session-core.ts`.
- `remote-session-protocol.ts` owns portable protocol types, validation, and
  wire-to-domain normalization.
- `remote-turn-coordinator.ts` owns active/queued turn correlation and the
  `SDKMessage` stream queue.
- `app-server-session.ts` adapts an app-server connection to the runtime
  controller used by the portable session core.
- `cloud-session.ts` resolves Cloud runtimes and managed sandboxes, then uses
  the same runtime controller and portable session core.
- `types.ts` defines the stable public SDK surface. Internal protocol types
  belong beside their implementation rather than in the public type barrel.

For `session.listMessages()` on an app-server or Cloud session, the call path
is:

`RemoteClientSessionCore.listMessages` →
`RemoteClientRuntimeController.listMessages` →
the selected transport adapter.

Those methods operate at different layers and should not be combined merely
because they share a name.

## Stream identifiers

Streamed `SDKMessage` values carry several identifiers, and each one answers a
different question. Most duplicate or missing message bugs in consumers come
from using one to answer another's question.

- `seqId` answers "have I already processed this stream position?" It is a
  per-run replay cursor: use it to suppress replayed events after a resume or
  reconnect, compare it only within the same `runId`, and reset the threshold
  when a new run starts. It is not row identity.
- `otid` answers "which typed message slice is this?" Use it to reassemble the
  fragments of one slice. It is not a universal key across message types.
- `uuid` answers "which message object is this?" Use it for message identity
  and for history cursoring and backfill. It is the top-level message ID when
  the stream provides one and an SDK-generated identifier otherwise, so it is
  neither a guaranteed server ID nor a replay cursor.
- `toolCallId` answers "which tool call is this about?" Use it to join a
  `tool_call` with its `tool_result`. It is not identity for assistant or
  reasoning messages.

Only `assistant` and `reasoning` carry `otid` and `seqId`; `tool_call` and
`tool_result` carry `toolCallId` and `uuid`. Identity therefore has to be
decided per message family. A single generic `seqId → otid → uuid` fallback
chain applied to every message will merge unrelated rows.

- Key `assistant` and `reasoning` accumulators on the message type together
  with `otid`, not on `otid` alone. A provider can reuse an `otid` across
  kinds, which collapses assistant text into reasoning. Fall back to `uuid`
  only within the same family — `src/tests/stream-message-identity.test.ts`
  pins the case where two slices share a `uuid` and differ only by type and
  `otid`.
- Merge tool arguments and results by `toolCallId`, but do not flatten every
  tool-family message onto it. Envelope identity and tool-payload identity are
  distinct.
- Do not merge persisted history into a transcript while a turn is streaming.
  When backfill after a reconnect is unavoidable, rebase the in-progress
  accumulators onto the fetched rows; otherwise later fragments append to a
  stale baseline and the row renders twice.
- Mid-stream, `tool_call.toolInput` is `{ raw: "<chunk>" }` until the argument
  fragments parse as JSON. Accumulate `rawArguments` and parse once at the end
  rather than overwriting parsed arguments with a partial wrapper.

## Cloud API ownership

- `@letta-ai/letta-code` owns the agent harness, app-server protocol, runtime
  lifecycle, and tool execution boundary.
- `@letta-ai/letta-client` owns Letta API HTTP contracts, authentication,
  headers, retries, pagination, and API errors. Declare it as a direct
  dependency when importing it; do not rely on Letta Code's transitive copy.
- Before adding a Cloud REST call, inspect the installed generated client. Use
  the generated resource method when it covers the endpoint.
- When an endpoint is not generated yet, use the shared Letta client's typed
  `get`/`post`/`patch`/`delete` method and validate the ungenerated response at
  that boundary. Do not add raw `fetch`, URL/auth/header helpers, response
  parsers, retry loops, or parallel HTTP error types.
- `LettaAgentClientBase` owns one lazy Cloud client and injects it into
  management, repository, environment, and session code. New Cloud REST
  surfaces should reuse that instance. WebSocket transport remains separate.
- Keep the public management types portable across Cloud and app-server
  backends. Generated Cloud types belong at the Cloud transport seam; do not
  narrow the shared facade to shapes that the app-server protocol cannot
  promise.
- Reuse generated Letta entity, request, and response types whenever the SDK
  contract has the same semantics. Camel-case SDK facades may rename fields,
  but their value types should derive from the generated params. Do not mirror
  generated entities as partial `Record<string, unknown>` shapes or rewrite
  app-server envelopes already exported by `app-server-protocol`.
- Verify generated route paths against the live API before introducing or
  preserving a generic-request exception. Server routing evolves: a mismatch
  proven in an older PR may no longer exist. Prefer the generated resource as
  soon as its live route works, and delete the workaround rather than
  fossilizing it in new architecture.

After changing this boundary, run `bun run check`, `bun test`, and
`bun run build`. If path behavior changed, run the narrow live route test too;
do not infer POST/DELETE slash behavior from a successful GET.

## Change discipline

- Keep transport parsing out of the session facade.
- Keep turn correlation and stream buffering in `RemoteTurnCoordinator`.
- Preserve exports from the package root and `/client` unless a change is
  explicitly intended to be breaking.
- Treat source-size exceptions in `scripts/check-source-size.ts` as debt
  ceilings: reduce them when a file shrinks and do not raise them casually.
- Run `bun run check`, `bun test`, and `bun run build` before opening a PR.
