---
title: Adapter Type Safety
description: SDK worker adapter type safety patterns - required fields, discriminated unions, and intentionally loose types
---

SDK worker adapters enforce correctness at compile time via TypeScript, not at deploy time. Misconfigured adapters fail in the IDE before a file is even saved -- no runtime surprises, no silent failures in production.

The approach: tighten types where misconfiguration is a realistic foot-gun, and leave types intentionally loose where flexibility is a design requirement.

---

## Required Field Enforcement

### LLM Adapter -- `provider` and `model`

`provider` and `model` are required on `SDKLLMGenerateParams` in `packages/sdk/src/worker/adapters/llm.ts`. They were previously optional because the server-side `modelConfig` fallback in `tool-dispatcher.ts` would fill them in -- but that fallback is a foot-gun, not a feature. Omitting them silently selects a default model that may not be appropriate for the call site.

```typescript
// Required -- both fields must be specified inline
await llm.generate({
  provider: 'anthropic',
  model: 'claude-sonnet-4-5',
  messages: [...],
  responseSchema: {...},
})
```

### Tomba Adapter -- `domain` and `email`

`domain` is required on `TombaDomainSearchParams` and `TombaEmailFinderParams`. `email` is required on `TombaEmailVerifierParams`. Previously all params were optional, allowing calls to compile but fail at runtime. The type change enforces the fields that are always necessary for email discovery and verification.

---

## Discriminated Unions

### Email Targeting -- mutually exclusive modes

`EmailToolMap.send.params` uses a discriminated union with `never` to enforce that exactly one targeting mode is provided. You cannot pass zero modes or combine multiple modes.

```typescript
type EmailTargeting =
  | { userIds: string[]; targetRole?: never; targetAll?: never }
  | { targetRole: string; userIds?: never; targetAll?: never }
  | { targetAll: true; userIds?: never; targetRole?: never }
```

The `never` pattern on sibling fields causes TypeScript to error if you pass two targeting fields simultaneously. Passing none also fails because none of the union members match.

---

## Strict Object Types

### Approval Adapter -- typed context

`ApprovalToolMap.create.params` uses `context: Record<string, unknown>` instead of `context: unknown`. This prevents passing primitives (strings, numbers) as context -- approval tasks always expect a structured key-value object.

```typescript
// Correct
await approval.create({
  context: { dealId: 'deal-123', proposalUrl: 'https://...' },
  ...
})

// TypeScript error -- primitives not assignable to Record<string, unknown>
await approval.create({ context: 'deal-123', ... })
```

---

## Intentionally Loose Types

Some adapters were audited and deliberately left with flexible types. Do not tighten these -- the looseness is a design requirement.

| Adapter | Field      | Type                        | Reason                                                                                                         |
| ------- | ---------- | --------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Attio   | `object`   | `string`                    | Supports custom user-defined object slugs; a union would be too restrictive for orgs with custom Attio objects |
| Attio   | `values`   | `Record<string, unknown>` | Dynamic per-object schemas vary by org configuration                                                           |
| PDF     | `document` | `Record<string, unknown>` | Renderer accepts flexible document structures by design; schema is enforced server-side                        |

---

**Source:** `packages/sdk/src/worker/adapters/llm.ts`, `packages/core/src/execution/engine/tools/tool-maps.ts`
