`connect.ai.*` acts for a **signed-in member of a workspace**: it answers from
that workspace's own corpus, and it stores a conversation against the person
who held it. The public counterpart — a stranger on your customer's marketing
site — is the [visitor assistant](/docs/visitor), and the two deliberately
share no store.

Four scopes, because four different outbound surfaces:

| Call | Scope |
| --- | --- |
| `chat`, `listConversations`, `getConversation`, `deleteConversation` | `ai:chat` |
| `extract` | `ai:extract` |
| `draftAssessment`, `reviewAssessment` | `ai:assess` |
| `readiness` | `ai:readiness` |

## The panel

```tsx
import { ConnectAskAi } from "@cosmoops/connect-sdk/client";

<ConnectAskAi
  screens={[
    { path: "/deals/[dealId]", purpose: "Work one deal: stage, value, next step.",
      suggestions: ["What changed on this deal?", "Who is the contact?"] },
  ]}
  renderLink={({ href, className, children }) => (
    <Link href={href} className={className}>{children}</Link>
  )}
/>
```

**`screens` is what makes it your product's assistant.** Without them it
answers from general knowledge about software, because Connect has no idea
what your application is. With them, "what can I do here?" answers in your
own words, an empty panel offers your own opening questions, and actions can
link into your routes. Write dynamic segments in brackets — `/deals/[dealId]`
— so a live path with a real id still matches.

They are **prose and links only**. Connect never uses a screen description to
decide what anyone may do, and never executes anything on your behalf.

Leave `activePath` unset in a Next app: the panel resolves the live path
itself through a `useSyncExternalStore` whose server snapshot is empty, so it
works inside a shared App Shell where `usePathname()` is refused outright
(`CLIENT_HOOK_DYNAMIC`). Pass it only where your router's path is not
`location.pathname` — a hash router, or a panel under a basePath your routes
do not include.

## Chat, server-side

```ts
const response = await connect.ai.chat({
  workspaceId,
  messages,
  asUser: sessionToken,
  conversationId,
});
```

It returns the **raw streaming `Response`**, not a parsed body — the endpoint
answers server-sent events, and buffering here would throw away the only
reason to stream. Pipe `response.body` onward, or hand it to the AI SDK's own
reader.

`asUser` takes a signed-in member's session token, and Connect then resolves
the workspace from *that*, ignoring `workspaceId`. Without it you are the
application, naming the workspace yourself. The token travels as a header and
never reaches the body.

## History belongs to a person

`conversationId` is **your own** id for a conversation, sent with every turn
of it. Connect upserts against it, and that is what makes the conversation
resumable.

It is persisted **only alongside `asUser`** — a conversation belongs to
someone, and a server-to-server call acts as nobody. Omit the id and the turn
stays out of history entirely, which is how a caller opts out.

```ts
const { items } = await connect.ai.listConversations({ asUser: sessionToken });
const detail = await connect.ai.getConversation({ conversationId, asUser: sessionToken });
await connect.ai.deleteConversation({ conversationId, asUser: sessionToken });
```

- `asUser` is **required** on all three, unlike on `chat`: the rows are scoped
  to a person and there is no application-wide view to fall back to.
- The list is cursor-paginated with **no total** — it is ordered by last use,
  which moves while it is being read.
- `turns[].parts` comes back exactly as the model produced it — prose,
  reasoning and tool calls in order — so it can be handed straight to a chat
  UI. A conversation that is not this user's, or not your application's, is a
  `404` either way.
- `deleteConversation` is idempotent: one already gone answers
  `{ deleted: true }`, so a retry after a dropped response does not read as a
  failure.

## Extraction

```ts
const fields = await connect.ai.extract<{ name: string; phone: string }>({
  schemaId: process.env.CRM_AI_CAPTURE_SCHEMA_ID!,
  fileUrl,
});
```

Structured JSON out of a file or image URL, against a **platform** extraction
schema referenced by id — Connect holds the prompt and the JSON Schema, your
application holds only the id. So the wording that reads a business card is
tuned in one place for every application that reads one, and there is no
prompt in your repo to drift.

A practical consequence worth copying: treat the id as **deployment
configuration**, and draw no control at all when it is unset. An environment
without a schema id should render no camera button rather than one that always
fails.

## Readiness

How ready the site at a domain is for AI agents: it probes for an MCP server,
an OpenAPI description, `llms.txt`, structured data and a markdown twin,
evaluates `robots.txt` against the assistants that actually fetch pages, and
checks whether the content survives without JavaScript.

```ts
const result = await connect.ai.readiness({ domain: "acme.com" });
if (result.status === "assessed") {
  console.log(result.report.grade, result.report.topFix?.fix);
}
```

- **Set your client timeout to at least 90 seconds.** A cold assessment runs
  the whole probe before answering. Cached reads return in milliseconds and
  are the normal case — reports are held for **a week**, not the ninety days a
  brand profile gets, because a report is a to-do list somebody is working
  through. Pass `refresh` after shipping a fix to see it land.
- **A check with `status: "error"` is unknown, not failed.** Connect could not
  measure it, and it is excluded from the score rather than counted against
  it.
- **`skipped` says whose problem it is.** `unreachable` means the site did not
  answer; `provider_unavailable` means Connect's own service did. Only the
  second is worth retrying silently, and neither should be rendered as a
  failing grade.

`draftAssessment` and `reviewAssessment` round out the surface: they draft
every section of a platform-defined assessment from one sentence, then review
the answers. Both are unattributed — assessments run against platform-wide
definitions, and the first consumers run before anyone has signed in, so there
is no `asUser` and no workspace. The intended flow is draft first and correct
section by section: a blank questionnaire is why most people abandon one, and
editing a strawman is a far smaller ask than authoring from nothing.

## Next steps

- **[Visitor assistant](/docs/visitor)** — the same idea for someone with no
  account, answering from a separate published store.
- **[MCP](/docs/mcp)** — register an endpoint of your own so Connect's
  assistant can call *your* tools, as the member asking.
- **[API reference](/docs/api/ai)** — the endpoints and their schemas.