# Threads (/docs/threads)

> Multi-turn conversations stored alongside runs.

`plico serve` stores threads and user messages in the same SQLite database as runs.
Create a thread with `POST /v1/threads` (HTTP 201), list threads with
`GET /v1/threads`, and read one at `GET /v1/threads/:id`. Post a message as
`{ "content": "Hello" }` to `POST /v1/threads/:id/messages`, and read its history
with `GET /v1/threads/:id/messages`.

## Messages and runs

When a provider is configured, posting a message reserves and starts a run and
returns its `runId`. The run uses earlier thread messages as conversation history,
and its successful answer is recorded as an assistant message.

- Only one run per thread may be active. A concurrent message receives
  `thread.busy` (HTTP 409).
- Stream run events at `GET /v1/runs/:runId/events/stream`. Reconnect with
  `Last-Event-ID` or `?after=`.
- Without a configured provider the endpoint returns an actionable error. To store a
  message without starting a run, include `"startRun": false`.

## Titles

Threads get a `title` once the conversation has a topic. After each of a thread's
first three replies, Plico names it from the words the user used, so a greeting such
as "Hi" leaves it untitled until there is something to name. The rule runs locally
with no model call, and the assistant's text can only confirm a topic, not add one.

`PATCH /v1/threads/<thread-id>` with `{ "title": "..." }` (up to 200 characters)
renames a thread. A title set that way is never replaced. Studio shows titles in the
thread list and the chat header, where a pencil icon renames the conversation.

### Model-written and custom titles

Keyword titles are the default. To have the configured model write the title instead, set
`threads: { title: "model" }` in `plico.config.ts`. That costs one short model call after each of a
thread's first three replies. For full control, add `threads/title.ts` and export a function by
default; it receives the messages (`{ role, content }[]`) and returns the title, and it takes
priority over the `title` setting:

```ts title="threads/title.ts"
export default async (messages: Array<{ role: string; content: string }>) =>
  messages[0]?.content.slice(0, 40);
```

The keyword title is stored first, so a model or function that fails, or returns nothing,
leaves it in place and never fails the run. A title the user set is never replaced.

## Recovery

If a server stops after reserving a run but before starting it, `servePlico` resumes
it only for the same project and provider account. A model change alone does not
prevent recovery. A mismatched account leaves the message queued rather than sending
its content to a different account.

## Embedding

A host that embeds Plico calls `createPlico({ projectRoot, databasePath })` from
`@plico-ai/server`. It opens the thread store, run store, engine and provider from the
project and the database path, recovers interrupted runs, resumes queued ones, and
returns `plico.threads` (create, get, list, rename, messages, send), `plico.runs`
(start, get, list, cancel, events) and `plico.approvals`. `plico.runs.events(runId, { after })`
is an async iterator over the run's events. Call `await plico.close()` before the process
exits; it ends open event iterators and closes the thread store. `servePlico` is
`createPlico` plus an HTTP listener and closes Plico when the server closes. Readiness
reports a thread and run store mismatch before a run is started.

## Mount the API in your backend

`plico.fetch(options)` returns a web-standard `(request) => Promise<Response>` handler for Hono, Next.js route handlers, Bun and Deno. `plico.node(options)` returns a Node `(req, res)` handler for Express, Fastify and plain `http`; it reads a body the host already parsed into `req.body`, such as with `express.json()`. Both serve the `/v1` API. Run your own authentication first, then let `scope` keep each request to its own data:

```ts
const plico = await createPlico({ projectRoot, databasePath });

app.use("/plico", requireLogin, plico.node({
  scope: (req) => ({ userId: req.headers.get("x-user-id") ?? "" }),
  context: (req) => ({ actor: req.headers.get("x-user-id") ?? "" }),
}));
```

- `scope` is a JSON object the host computes per request. Plico stamps it on new threads and runs as their `metadata` and refuses every read and action on anything it does not match, with a `404`, including events, artifacts, cancel and approvals. A client cannot widen it or set its own `metadata`. A `scope` function that throws or returns anything but an object fails the request with a `500`.
- A scoped mount does not serve the project-level routes `/v1/project`, `/v1/instructions`, `/v1/eval`, `/v1/readiness` and `/v1/runs/dry`.
- `context` is host data for the runs a request starts. Tools read it as `ctx.context`, `run.started` records it, and an approval sent through the mounted API is recorded with it as `approval.decided.context`; a client cannot supply its own.
- `basePath` removes a mount path such as `/plico` before routing, for hosts that do not strip it. `publicOrigin` and `allowScript` work as on `servePlico`.
- A mounted API checks `Content-Type` and refuses cross-origin requests, but not the loopback `Host`. The host owns authentication and permissions.

## Host data: metadata and context

Plico has no user model. A host that serves its own users passes two kinds of data and
enforces access itself.

`metadata` is a JSON object kept with a thread or run, set when it is created and never
changed. Use it to tag work with your own identifiers, such as a tenant or a ticket.
`plico.threads.create({ metadata })` and `plico.runs.start({ metadata })` set it, and a
thread's runs carry the thread's metadata. Lists filter by exact match on top-level keys:
`plico.threads.list({ metadata: { tenant: "acme" } })`, or over HTTP
`GET /v1/threads?metadata=<url-encoded JSON>` and `GET /v1/runs?metadata=<url-encoded JSON>`.
A key matches when its value is equal as JSON; a filter with several keys needs all of them.
Metadata that is not a JSON object is rejected with `request.invalid`.

`context` is JSON the host passes for one run: `plico.threads.send(threadId, text, { context })`
or `plico.runs.start({ context })`. Tools read it as `ctx.context`. Plico records it on the
run's `run.started` event, so a run that resumes after a restart, including one queued before
the restart, gets the same context. Because the event log stores it, `context` must not carry
secrets: pass an identifier and look up credentials in your tool. `plico.approvals.decide(runId,
requestId, approved, { context })` records the deciding context on `approval.decided`. The HTTP
API does not accept `context`; an approval signal sent to `POST /v1/runs/<run-id>/signal`
has any `context` dropped, so only the host can say who decided.