diff --git a/.changeset/ai-tool-helpers.md b/.changeset/ai-tool-helpers.md new file mode 100644 index 00000000000..09e3b612ada --- /dev/null +++ b/.changeset/ai-tool-helpers.md @@ -0,0 +1,15 @@ +--- +"@trigger.dev/sdk": patch +--- + +Add `ai.toolExecute(task)` so you can wire a Trigger subtask in as the `execute` handler of an AI SDK `tool()` while defining `description` and `inputSchema` yourself — useful when you want full control over the tool surface and just need Trigger's subtask machinery for the body. + +```ts +const myTool = tool({ + description: "...", + inputSchema: z.object({ ... }), + execute: ai.toolExecute(mySubtask), +}); +``` + +`ai.tool(task)` (`toolFromTask`) keeps doing the all-in-one wrap and now aligns its return type with AI SDK's `ToolSet`. Minimum `ai` peer raised to `^6.0.116` to avoid cross-version `ToolSet` mismatches in monorepos. diff --git a/.changeset/mcp-agent-chat-sessions.md b/.changeset/mcp-agent-chat-sessions.md new file mode 100644 index 00000000000..c3f01aebf28 --- /dev/null +++ b/.changeset/mcp-agent-chat-sessions.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +The CLI MCP server's agent-chat tools (`start_agent_chat`, `send_agent_message`, `close_agent_chat`) now run on the new Sessions primitive, so AI assistants driving a `chat.agent` get the same idempotent-by-`chatId`, durable-across-runs behavior the browser transport gets. Required PAT scopes go from `write:inputStreams` to `read:sessions` + `write:sessions`. diff --git a/.claude/architecture/chat-agent-sessions.md b/.claude/architecture/chat-agent-sessions.md new file mode 100644 index 00000000000..18c41a26e0c --- /dev/null +++ b/.claude/architecture/chat-agent-sessions.md @@ -0,0 +1,416 @@ +# chat.agent on Sessions — architecture reference + +Snapshot of how `chat.agent` works after the Session migration. Meant +to orient Claude sessions and writers of `docs/ai-chat/…` without +having to re-derive the design from the code. + +Scope: everything in this document applies to the `ai-chat` PR +(`feature/tri-7532`, on top of `feature/tri-8627`). Neither is merged +yet. Once shipped, the old CHAT_STREAM_KEY / CHAT_MESSAGES_STREAM_ID / +CHAT_STOP_STREAM_ID constants are deleted and the three remaining +legacy consumers (MCP `agentChat` tool, `mock-chat-agent`, +dashboard `AgentView.tsx`) are migrated too. + +## Why + +Pre-migration, `chat.agent` ran entirely on run-scoped primitives: + +- Output: one `streams.writer("chat")` on the current run. +- Input: two `streams.input()` definitions — `"chat-messages"` and + `"chat-stop"`. +- The browser transport subscribed to + `/realtime/v1/streams/{runId}/chat` and POST-ed to run-scoped + input-stream URLs. `ChatSession` persistence was `{runId, + publicAccessToken, lastEventId}`. + +Every durable identity was the `runId`. That blocked: + +- Resuming a chat across runs (run ends → session dies). +- Listing/filtering a user's chats (no `chatId → runId` inbox). +- Cross-tab and cross-device coordination beyond a single run. +- Moving chat state between tasks without smuggling it through run + metadata. + +Sessions give us a durable `{sessionId, externalId}` pair that +outlives any one run, plus a bidirectional typed channel pair +(`.in` / `.out`). The migration rebuilds `chat.agent`'s I/O on top +of Sessions with zero surface-level change to the public +`chat.agent()` / `TriggerChatTransport` / `AgentChat` APIs. + +## The Session primitive (2-minute version) + +Lives in `feature/tri-8627`. See `packages/core/src/v3/sessions.ts` +and `apps/webapp/app/routes/(api|realtime).v1.sessions*`. + +- `sessions.create({type, externalId, …})` — Postgres upsert on + `(environmentId, externalId)`. Idempotent. +- `sessions.open(id)` — returns a `SessionHandle { id, in, out }`. + No network call until you hit a channel method. +- `.out` is a `SessionOutputChannel` — **producer-side API**: + `append` (single record), `pipe(stream)`, `writer({execute})` + (matches `streams.define`), plus `read(options?)` for external + SSE consumers. All three producer methods route through + `SessionStreamInstance` → `StreamsWriterV2` → direct-to-S2 so + subscribers see a uniform parsed-object shape. +- `.in` is a `SessionInputChannel` — **consumer-side API for the + task**: `on`, `once`, `peek`, `wait`, `waitWithIdleTimeout` + (matches `streams.input`), plus `send(value)` for external + producers. `.wait` / `.waitWithIdleTimeout` suspend the run + through a **session-scoped waitpoint** — same mechanism as + `streams.input.wait`, but the waitpoint fires when a record + lands on the session's `.in` instead of a run's input stream. +- The two channels have **zero overlapping method names** — + directional intent always stays at the call site. +- Session channels accept either the friendlyId (`session_*`) or + the user-supplied externalId. The server disambiguates via the + `session_` prefix. + +## The chat mapping + +One Session per chat conversation: + +``` +SessionHandle (durable identity, outlives runs) +├── .in — chat messages + stops (tagged ChatInputChunk) +└── .out — UIMessageChunks + control chunks + +externalId = chatId (client-owned, human-meaningful) +friendlyId = session_xxxxxxxxxxxx (generated, stable) +type = "chat.agent" +``` + +A session's `.in` carries a discriminated union — +`ChatInputChunk` in `packages/trigger-sdk/src/v3/ai.ts`: + +```ts +type ChatInputChunk = + | { kind: "message"; payload: ChatTaskWirePayload } + | { kind: "stop"; message?: string }; +``` + +The task dispatches on `chunk.kind`. The message payload is the same +`ChatTaskWirePayload` the run originally received — so a +message-kind chunk at turn N mirrors the shape of turn 0's payload. + +`.out` carries UIMessageChunks (token streaming) interleaved with +control chunks (`trigger:turn-complete`, `trigger:upgrade-required`) +and `chat.store` deltas. Semantically unchanged from pre-migration — +only the transport (S2 via Session) changed. + +## End-to-end flow (first message) + +``` + Browser Server action Webapp / S2 Agent run + ──────── ───────────── ────────── ───────── + useChat.sendMessage + → transport.sendMessages + → triggerTaskFn (if set) + sessions.create + externalId = chatId + type = "chat.agent" + → session_xxx + tasks.trigger( + "my-chat-agent", + { chatId, sessionId, + messages, trigger, + metadata }) + auth.createPublicToken({ + read: { runs, sessions }, + write: { inputStreams, sessions } }) + → { runId, publicAccessToken, + sessionId } + ← sessions.set(chatId, state) + → subscribeToSessionStream + GET /realtime/v1/sessions/{sessionId}/out + [SSE open] + run starts + payload.sessionId + locals.set(chatSessionHandleKey, + sessions.open(sessionId)) + onChatStart() + run() → streamText(…) + pipeChat(uiStream) + → chatStream.pipe + → session.out.pipe + → SessionStreamInstance + → StreamsWriterV2 + → S2 + [records land on S2] + ← SSE chunks stream [SSE delivers chunks] + id: 0 start + id: 1 start-step + id: 2 text-start + id: 3… text-delta + … + writeTurnCompleteChunk() + via chatStream.writer + id: N trigger:turn-complete await messagesInput + .waitWithIdleTimeout(…) + — run suspends on the + session-stream waitpoint +``` + +## Subsequent turns (run still live) + +``` + Browser Agent run (suspended) + ──────── ───────────────────── + transport.sendMessages (same chatId) + state.runId is set → "existing run" branch + → POST /realtime/v1/sessions/{sessionId}/in/append + body: {"kind":"message","payload":{…}} + session append handler + drain waitpoints set + → complete waitpoint + run resumes with + next message + turn-complete chunk + → session.out + [SSE delivers chunks] + ← chunks … +``` + +## Subsequent turns (previous run ended) + +Transport detects `state.runId` is gone (or append fails). Re-triggers a +new run on the same session — `sessionId` stays, only `runId` + PAT +refresh. Upgrade-required has the same shape. + +## Stop + +``` + Browser Agent run (streaming) + ──────── ───────────────────── + transport.stopGeneration(chatId) + → POST /realtime/v1/sessions/{sessionId}/in/append + body: {"kind":"stop"} + session append handler + → complete waitpoint + → deliver to stopInput.on() + currentStopController.abort() + streamText aborts + turn ends early, + trigger:turn-complete + emitted on .out + run returns to idle wait +``` + +`stopInput` is a module-level facade that filters `.in` for +`kind === "stop"`. The run's persistent listener fires on every stop +regardless of whether a turn is active. + +## Module layout (SDK) + +``` +packages/trigger-sdk/src/v3/ +├── ai.ts chat.agent factory. Module-level facades: +│ chatStream : RealtimeDefinedStream +│ messagesInput: RealtimeDefinedInputStream +│ stopInput : RealtimeDefinedInputStream<{stop, message?}> +│ Facades resolve `getChatSession()` at call time. +│ chat.stream / chat.messages re-export them for users. +│ Locals slot: chatSessionHandleKey. +│ Initialized at run start from payload.sessionId +│ (falls back to payload.chatId). +├── chat.ts TriggerChatTransport. Calls: +│ apiClient.createSession (ensureSession) +│ apiClient.appendToSessionStream(..., "in", chunk) +│ GET /realtime/v1/sessions/{sessionId}/out (SSE) +│ ChatSessionState keys on sessionId, runId optional. +├── chat-client.ts Server-side AgentChat + ChatStream. +│ Same shape as TriggerChatTransport but uses the +│ env secret key (apiClientManager.accessToken) so +│ session CRUD doesn't need extra auth wiring. +├── sessions.ts SessionHandle / SessionInputChannel / +│ SessionOutputChannel. Thin SDK over the core +│ ApiClient session methods + sessionStreams API. +``` + +## Module layout (core) + +``` +packages/core/src/v3/ +├── schemas/api.ts Session CRUD + waitpoint schemas +├── apiClient/index.ts createSession / appendToSessionStream / +│ subscribeToSessionStream / +│ initializeSessionStream / +│ createSessionStreamWaitpoint +├── sessionStreams/ +│ ├── types.ts SessionStreamManager interface +│ ├── noopManager.ts +│ ├── manager.ts StandardSessionStreamManager — +│ │ SSE tail + once/on/peek buffer +│ │ keyed on `{sessionId, io}` +│ └── index.ts SessionStreamsAPI facade +├── session-streams-api.ts `sessionStreams` global singleton +└── realtimeStreams/ + └── sessionStreamInstance.ts SessionStreamInstance — S2-only + parallel of StreamInstance. Used + by SessionOutputChannel.pipe/writer. +``` + +## Module layout (webapp) + +``` +apps/webapp/app/ +├── routes/ +│ ├── api.v1.sessions*.ts CRUD (create/list/retrieve/update/close) +│ ├── realtime.v1.sessions.$session.$io.ts SSE subscribe + HEAD (last-seq) +│ ├── realtime.v1.sessions.$session.$io.append.ts +│ │ POST append — fires pending +│ │ session-stream waitpoints after +│ │ each record lands +│ └── api.v1.runs.$runFriendlyId.session-streams.wait.ts +│ POST create-waitpoint. Race-checks +│ the S2 stream at lastSeqNum so +│ pre-arrived data fires the +│ waitpoint synchronously. +├── services/ +│ ├── realtime/ +│ │ ├── sessions.server.ts resolveSessionByIdOrExternalId, +│ │ │ serializeSession +│ │ └── s2realtimeStreams.server.ts appendPartToSessionStream, +│ │ readSessionStreamRecords, +│ │ streamResponseFromSessionStream +│ ├── sessionStreamWaitpointCache.server.ts Redis set keyed on +│ │ `ssw:{sessionFriendlyId}:{io}`; +│ │ drained atomically on append +│ └── sessionsReplicationService.server.ts Postgres → ClickHouse sessions_v1 +``` + +## Token scopes + +The PAT minted for the browser transport carries **both** run and +session scopes — so a single token covers every session-side call the +transport makes (append, subscribe) plus any remaining run-scoped +fallbacks: + +``` +{ + read: { runs: runId, sessions: sessionId }, + write: { inputStreams: runId, sessions: sessionId }, +} +``` + +Three mint sites in `ai.ts`: + +- `createChatTriggerAction` (server-side `triggerTask` helper — + creates the session before triggering, returns `sessionId` in the + result). +- `preloadAccessToken` (agent-side, per-preload). +- `turnAccessToken` (agent-side, refreshed each turn, delivered via + the `trigger:turn-complete` chunk's `publicAccessToken` field). + +The server-side `AgentChat` / `ChatStream` path uses the environment +secret key directly — no per-run tokens needed. + +## Key invariants + +- **Sessions outlive runs.** Session close is client-driven; the + task runtime never auto-closes. +- **`.in` and `.out` are disjoint.** No method appears on both + channels; directional intent is always at the call site. +- **Uniform serialization on `.out`.** `append`, `pipe`, `writer` all + route through `StreamsWriterV2` so subscribers always receive + parsed objects, never raw JSON strings. +- **Suspend-while-idle on `.in`.** Session-stream waitpoints use the + same run-engine mechanism as input-stream waitpoints — no compute + is consumed between turns. +- **One run per active turn.** The transport's first-message path + triggers a run; subsequent messages land via `.in.send(...)` + against the same run (or spawn a new run on the same session if + the previous one ended). + +## Public API surface (what changed / what's the same) + +Unchanged: + +- `chat.agent({ id, run, onChatStart, … })` +- `chat.stream`, `chat.messages`, `chat.createStopSignal` +- `chat.store.set / patch / get / onChange` +- `chat.response.write`, `chat.defer`, `chat.history`, etc. +- `TriggerChatTransport` options and methods +- `AgentChat` server-side API + +Grown: + +- `ChatTaskWirePayload` / `ChatTaskPayload` / `ChatTaskRunPayload` + gain optional `sessionId`. +- `TriggerChatTaskResult` gains optional `sessionId`. +- `TriggerChatTransport.getSession` / `setSession` / `onSessionChange` + / `sessions` options all carry `sessionId`; `runId` is now optional. +- `AgentChat.ChatSession` persistence type gains `sessionId`. + +Added (public): + +- `sessions.create / retrieve / update / close / list / open` +- `SessionHandle`, `SessionInputChannel`, `SessionOutputChannel` + +## Known follow-ups + +Tracked on task #49 in the project task list: + +- Migrate three remaining legacy-stream consumers (still use + run-scoped stream URLs): + - `packages/cli-v3/src/mcp/tools/agentChat.ts` — MCP chat tool + Claude uses to talk to agents. + - `packages/trigger-sdk/src/v3/test/mock-chat-agent.ts` — the + offline test harness. Needs a `TestSessionStreamManager` plus + a pipe/writer sink in `mock-task-context.ts` since the agent + now writes through `SessionStreamInstance` (direct-to-S2) which + the current output-inspection driver doesn't intercept. + - `apps/webapp/app/components/runs/v3/agent/AgentView.tsx` — the + dashboard's per-run agent viewer. Still subscribes to + `/realtime/v1/streams/{runId}/chat`. +- Delete `CHAT_STREAM_KEY` / `CHAT_MESSAGES_STREAM_ID` / + `CHAT_STOP_STREAM_ID` from `packages/core/src/v3/chat-client.ts` + + `packages/trigger-sdk/src/v3/chat-constants.ts` + re-exports in + `ai.ts` once those three consumers are migrated. +- Full UI smoke in `references/ai-chat` (send / stop / refresh-resume + / multi-turn / cross-run resume). Core end-to-end flow already + validated via `chat-agent-smoke` in `references/hello-world`. + +## Smoke tests + +- `sessions-smoke` (`references/hello-world/src/trigger/sessionsSmoke.ts`) + — control plane + `.out.writer` + `.out.append` + `.in.send` + + list / pagination / close / idempotent close. +- `sessions-wait-smoke` (`references/hello-world/src/trigger/sessionsWaitSmoke.ts`) + — full waitpoint suspend/resume path. Orchestrator suspends on + `.in.waitWithIdleTimeout`; delayed sender fires the waitpoint via + `.in.send`; orchestrator resumes with the payload. +- `chat-agent-smoke` (`references/hello-world/src/trigger/chatAgentSmoke.ts`) + — end-to-end chat.agent flow. Creates a session, triggers + `test-agent` with `{chatId, sessionId, messages, …}`, subscribes to + `session.out`, asserts 14 UIMessageChunks (`start` / + `start-step` / `text-start` / 7× `text-delta` / `text-end` / + `finish-step` / `finish` / `trigger:turn-complete`) with ids 0–13. + Requires `OPENAI_API_KEY` in the dev env. + +## Git trail + +Sessions branch (`feature/tri-8627-session-primitive-server-side-schema-routes-clickhouse`): + +``` +4cadc19 feat(webapp,core): Session channel waitpoints — server side +95f3c00 fix(webapp): tighten sessions create + list auth +829ccc4 fix(webapp): allow JWT + CORS on sessions list endpoint +27fb4a4 fix(core): reject externalId starting with 'session_' on Session create/update +6f9dbe5 code review fixes +16ee28f feat(webapp,clickhouse,database,core): Session primitive (server side) +``` + +AI-chat branch (`feature/tri-7532-ai-sdk-chat-transport-and-chat-task-system`): + +``` +7aa6687 fix(sdk,chat): route pipeChat through session.out + chat-agent smoke test +762ed92 feat(sdk): server-side ChatStream / AgentChat → Sessions (phase D) +91b0481 feat(sdk): chat.agent → Sessions migration (phases B + C + min E) +e72555b feat(sdk,core): Session channel SDK toolkits + waitpoints — client side +0191302 feat(sdk,core): Session client SDK + hello-world smoke test +``` + +Later commits on the chat branch (chat.store, hydrateMessages, +multi-tab coordination, tool approvals, etc.) pre-date the Session +migration and are unchanged by it — the migration changed plumbing, +not public surface. diff --git a/.claude/architecture/sessions-as-run-manager.md b/.claude/architecture/sessions-as-run-manager.md new file mode 100644 index 00000000000..41f81720a83 --- /dev/null +++ b/.claude/architecture/sessions-as-run-manager.md @@ -0,0 +1,558 @@ +# Sessions as run manager + +Plan for the next chat.agent / Sessions branch. Builds on the row-agnostic +addressing branch (`chat-agent-sessions.md`). + +## Context + +The previous branch made `chatId` (the externalId) the universal addressing +string and made the `.in/.out/wait` routes row-agnostic. It works, but the +transport still owns run lifecycle: it triggers the first run, threads +`runId` through state, has to detect "run died" so the next user message +re-triggers, and re-triggers explicitly on `trigger:upgrade-required`. + +Two real gaps fall out of that: + +1. **Run-death blindness.** `.in/append` is run-independent — it appends to + S2 successfully whether or not the run is alive. The transport's + "non-auth error → re-trigger" fallback (`chat.ts:647-654`) is dead code + under row-agnostic addressing because the endpoint always 200s. If a run + is cancelled or crashes mid-turn before emitting `turn-complete`, the + user's next message sits in S2 with no listener and the transport has + no signal to recover. + +2. **Transport carries upgrade plumbing.** ~50 lines around + `subscribeToSessionStream`'s `upgradeRetry`, threaded `payload`+ + `messages` for re-trigger, and the `triggerNewRun` call on a + client-issued retry — all so the transport can react to a chunk the + agent emits. Server is in a better position to do this work. + +The fix: **make Session the run manager.** Sessions know their task, +their config, and their current run. Server triggers/re-triggers as +needed. Browser holds a session-scoped PAT and never sees runs. + +Nothing has shipped yet — no back-compat needed. We're free to break +public surface (`chat.createTriggerAction`, `onSessionChange` shape, +`ChatSession.runId`). + +## Design + +### Invariants + +- Session is the durable identity of a chat. One session, many runs over + its lifetime. +- Session always knows its task (`taskIdentifier`) and how to trigger it + (`triggerConfig`). Sessions without those fields don't exist anymore — + Sessions are task-bound by design. +- At most one live run per session at a time. Tracked as + `Session.currentRunId` (non-FK, can lag reality). +- `Session.currentRunVersion` (monotonic int) drives optimistic locking on + any state transition that swaps the run. +- Browser only ever holds session-scoped tokens. Run identifiers are a + server-side implementation detail. +- The append-time probe is the source of truth. Hooks from run-engine into + Session are optional eager-clears for dashboard freshness, never for + correctness. + +### State machine + +``` + ┌─────────────────┐ + │ Session created │ + │ first run fired │ + └────────┬────────┘ + ▼ + ┌──────────────────┐ user msg / .in append + │ currentRun alive │ ◀────────────────────────┐ + └────────┬─────────┘ │ + │ run terminates │ + │ (idle, cancel, crash, end-cont.) │ + ▼ │ + ┌──────────────────┐ .in/append probes │ + │ currentRun stale │ ─── ensureRunForSession ─┘ + └──────────────────┘ + │ session.close() + ▼ + ┌──────────────────┐ + │ closed (terminal)│ + └──────────────────┘ +``` + +### Three trigger paths + +1. **Session create.** `POST /api/v1/sessions` creates the row and triggers + the first run synchronously, returns `{ id, runId, publicAccessToken }`. +2. **`.in/append` probe.** Server checks `currentRunId`'s snapshot status; + if terminal, calls `ensureRunForSession` before processing the append. +3. **`end-and-continue`.** Agent calls `POST /api/v1/sessions/:id/end-and-continue` + to request a clean handoff to a fresh run on the latest version. Server + triggers v2, swaps `currentRunId`, returns the new runId. v1 emits its + final `.out` chunks (e.g. `trigger:upgrade-required` for transport + telemetry) and exits. + +## Schema + +### Prisma changes + +```prisma +model Session { + // existing fields stay... + + // Now required (today nullable). Sessions are task-bound. + taskIdentifier String + + // New: trigger payload + options for re-runs. + // { basePayload, machine, queue, tags, maxAttempts, idleTimeoutInSeconds } + triggerConfig Json + + // New: current run pointer. Non-FK so run deletion doesn't cascade. + currentRunId String? + + // New: monotonic counter for optimistic locking on currentRunId swaps. + currentRunVersion Int @default(0) + + @@index([currentRunId]) // only useful for "find session by run" reverse lookups +} +``` + +### Optional historical join (defer to v1.1) + +```prisma +model SessionRun { + sessionId String + runId String @unique + reason String // "initial" | "continuation" | "upgrade" | "manual" + triggeredAt DateTime @default(now()) + + @@index([sessionId]) +} +``` + +Not strictly needed for v1 — debugging/audit can use TaskRun's existing +metadata + `Session.currentRunId` history via `git`-style logs in +ClickHouse if desired. Add only if a concrete dashboard surface needs it. + +### Migration + +Two-step: + +1. Add the new columns + populate `taskIdentifier` from existing data + (chat.agent sessions all have it implicit via tags or metadata). +2. Set `triggerConfig = '{}'` for any existing sessions and either close + them or leave them as zombies. Since the old transport still works + pre-merge, this branch is the cutover. + +For the dev DB: I'll write a backfill that closes existing dev sessions +rather than try to compute valid triggerConfigs for them. They were all +test data anyway. + +## API surface + +### `POST /api/v1/sessions` — modified + +Two auth modes: + +| Mode | Caller | Required scope | Notes | +| ----------- | ----------------------- | ----------------------------- | -------------------------------------------------- | +| Secret key | Customer's server | env-wide | `chat.createStartSessionAction` server action | +| One-time JWT| Browser | `trigger:tasks:{taskId}` | Mints via `auth.createTriggerPublicToken(taskId)` | + +Body (Zod-validated): + +```ts +{ + type: string, // existing + externalId?: string, // chatId for chat.agent + taskIdentifier: string, // required; must match scope if JWT + triggerConfig: { + basePayload: Record, + machine?: MachinePresetName, + queue?: string, + tags?: string[], // ≤5 + maxAttempts?: number, + idleTimeoutInSeconds?: number, + }, + tags?: string[], // existing — session-level tags + metadata?: Record, // existing +} +``` + +Response: + +```ts +{ + id: string, // session_* + runId: string, // first run, freshly triggered + publicAccessToken: string, // session-scoped, long TTL + externalId: string | null, + type: string, + // ... rest of SessionItem fields +} +``` + +Behavior: + +- Idempotent on `(env, externalId)`. Repeat calls return the existing + session, ensure-running its run if terminal, return a fresh PAT. +- Token consumption: if JWT mode, the one-time token is consumed on first + successful call (existing replay-protection infra). +- PAT scopes returned: `read:sessions:{externalId} + write:sessions:{externalId}`. + No run-scoped permissions — the transport doesn't need them. + +### `POST /api/v1/sessions/:id/in/append` — modified + +Add the probe + ensure-run step before the existing S2 append. Pseudocode: + +```ts +const sess = await readSession(id); +if (sess.closedAt) return 400; +if (sess.expiresAt && sess.expiresAt < now) return 400; + +if (!sess.currentRunId || isTerminal(await getSnapshotStatus(sess.currentRunId))) { + await ensureRunForSession(sess); // see below +} + +return appendToS2(addressingKey, body); // unchanged +``` + +The probe is one Redis snapshot read (`getSnapshotStatus` is cheap, +already used by the run-engine). Net hot-path overhead: ~1ms. + +### `POST /api/v1/sessions/:id/end-and-continue` — new + +Called by the run itself (uses internal run auth, scoped to the +calling run's id + the session id). Triggers a fresh run for the same +session, atomically swaps `currentRunId`, returns the new runId. + +Body: + +```ts +{ + reason: "upgrade" | "explicit-handoff" | string, + // optional metadata for SessionRun.reason if/when we add the join table +} +``` + +Response: + +```ts +{ runId: string } +``` + +The calling run is expected to exit shortly after receiving the response — +it has done whatever wrap-up it wanted and is delegating the conversation +to the new run. The transport sees this as "more chunks arrive on `.out`, +some from v1 then some from v2" — it's the same S2 stream keyed on chatId. + +### Other routes — unchanged + +`GET /api/v1/sessions/:id`, `PATCH /api/v1/sessions/:id` (close, update), +`PUT /realtime/v1/sessions/:id/:io`, `GET /realtime/v1/sessions/:id/:io` +(SSE subscribe, including the row-agnostic addressing from the previous +branch) — all stay the same. + +## Server internals + +### `ensureRunForSession` — atomic re-run via optimistic locking + +Lives in a new service: `apps/webapp/app/services/realtime/sessionRunManager.server.ts`. + +```ts +async function ensureRunForSession( + sess: SessionRow, + reason: "initial" | "continuation" | "upgrade" | "manual" +): Promise<{ runId: string }> { + // 1. Trigger the run upfront. Cheap to cancel if we lose the race. + const newRun = await triggerTaskInternal(sess.taskIdentifier, sess.triggerConfig); + + // 2. Try to claim the slot. + const claimed = await prisma.session.updateMany({ + where: { + id: sess.id, + currentRunVersion: sess.currentRunVersion, + }, + data: { + currentRunId: newRun.id, + currentRunVersion: { increment: 1 }, + }, + }); + + if (claimed.count === 1) { + // Optionally record SessionRun history here. + return { runId: newRun.id }; + } + + // 3. Lost the race. Cancel ours, reuse whoever won. + cancelTaskRun(newRun.id).catch(() => {/* fire-and-forget */}); + const fresh = await readSession(sess.id); + if (fresh.currentRunId && !isTerminal(await getSnapshotStatus(fresh.currentRunId))) { + return { runId: fresh.currentRunId }; + } + + // 4. Pathological: winner's run died between win and our re-read. Recurse. + return ensureRunForSession(fresh, reason); +} +``` + +Key properties: +- No DB lock held across the trigger network call. +- Wasted-trigger window is small and bounded (multi-tab race on dead run, + ms apart). Cancel cost is negligible. +- Recursion only on pathological double-failure; bounded by run-engine's + own progress. + +### Run-engine eager-clear (optional, defer) + +A run-engine post-termination hook that nulls `Session.currentRunId` when +the terminal run matches. Purely a dashboard freshness concern. Skip in +v1 — append-time probe is the source of truth. + +## SDK changes + +### Transport (`packages/trigger-sdk/src/v3/chat.ts`) + +State collapses to: + +```ts +type ChatSessionState = { + publicAccessToken: string; // session-scoped, long TTL + lastEventId?: string; // for SSE resume + isStreaming?: boolean; // for reconnect-on-reload UX + skipToTurnComplete?: boolean; // for stop+resume UX +}; +``` + +Note: no `runId`, no `sessionId`. The chat is the chatId; the token is +session-scoped. + +Removed: +- `triggerTaskFn` callback option (constructor branch on it) +- `triggerNewRun()` method +- `renewRunPatForSession()` +- `renewRunAccessToken` callback option (token is session-scoped, doesn't + expire on run boundaries) +- `ensureSession()` (already removed in previous branch) +- The `trigger:upgrade-required` re-trigger handler in + `subscribeToSessionStream` (~50 lines) +- The `upgradeRetry: { payload, messages }` parameter threaded through + `sendMessages`, `preload`, `subscribeToSessionStream` +- The non-auth-error fallback in `sendMessages` (dead code, removed) + +Renamed/replaced: +- `chat.createTriggerAction` → `chat.createStartSessionAction` + - Calls `sessions.create({ taskIdentifier, externalId, triggerConfig })` + server-side with secret key + - Returns `{ publicAccessToken }` (no runId — invisible to browser) + +New methods: +- `transport.start(chatId, opts)` — for the browser-mediated path: + - Customer provides a `getStartToken(taskId)` callback that mints the + one-time JWT + - Transport calls `POST /sessions` with that token + - Receives session PAT, stores as state.publicAccessToken +- `transport.preload(chatId)` — same shape as `start` but with empty + basePayload override + +Method behavior changes: +- `sendMessages` — no trigger logic. Always `.in/append`. Server triggers + if needed. On 401/403, error out (token expired — customer's token + callback should provide fresh). +- `subscribeToSessionStream(chatId)` — pure passthrough on `.out`. Filters + `trigger:upgrade-required` for cleanliness (server handles the re-run + swap). Filters `trigger:turn-complete` as today. +- `stopGeneration` — `.in/append` with `{ kind: "stop" }`. Unchanged. +- `getSession(chatId)` — returns `{ publicAccessToken, lastEventId, isStreaming }`. + No id fields. + +### `chat-client.ts` (server-side AgentChat) + +Mirror the transport: state without `runId`/`sessionId`, no `triggerNewRun`, +constructor takes `{ chatId, publicAccessToken }` (or mints via secret +key). All `.in/append` and `.out` URLs use `chatId`. + +### `chat.agent` runtime (`packages/trigger-sdk/src/v3/ai.ts`) + +- Drop the fire-and-forget `sessions.create({ externalId: chatId })` at + bind. Session already exists by the time the agent boots — server + triggers via `ensureRunForSession` after creating the row. +- Keep `sessions.open(payload.chatId)` for helper resolution. No change. +- `chat.requestUpgrade()` plumbing: calls `POST /sessions/:id/end-and-continue` + with the run's internal auth. On success, emits `trigger:upgrade-required` + on `.out` for telemetry, exits cleanly. + +### Reference projects (`references/ai-chat`) + +- `actions.ts`: replace `chat.createTriggerAction` callsite with + `chat.createStartSessionAction` +- `chat-app.tsx`: pass the new `start` mode to `useTriggerChatTransport` +- `chat.tsx`: drop `runId` references +- `trigger/chat.ts`: no changes (chat.agent contract unchanged from + agent-author POV) + +## Auth model summary + +| Token | Scopes | Where minted | Lifetime | +| ----------------------------- | ------------------------------------------------------ | ----------------------------------------- | ----------- | +| Trigger-task one-shot | `trigger:tasks:{taskId}` | `auth.createTriggerPublicToken(taskId)` | One use | +| Session PAT | `read:sessions:{ext} + write:sessions:{ext}` | Issued by `POST /sessions` | 1h–24h | +| Run-internal PAT (chat.agent) | `read:runs:{run} + read:sessions:{ext} + …` | Server-side, never crosses to browser | Run-bounded | + +Browser holds at most a one-shot token (briefly) and a session PAT +(steady state). Never holds a run-scoped token. + +## Edge cases + +- **Concurrent multi-tab on dead run** — optimistic locking handles it, + loser cancels its triggered run. +- **Page refresh mid-stream** — `.out` SSE resumes via Last-Event-ID + (existing); session PAT survives because it's not run-scoped. +- **Run cancelled by user (dashboard)** — append-time probe sees terminal, + triggers new run on next message. +- **Idle exit** — same path; user comes back later, sends message, fresh + run boots. +- **Crash mid-turn (no `turn-complete` emitted)** — same path; persisted + store is pre-turn, fresh run reads `.in` from tail position, picks up + unanswered message. +- **Upgrade during user message** — optimistic locking in + `end-and-continue` ensures one wins. If user message wins, + `end-and-continue` returns conflict, agent v1 keeps running, processes + message, retries upgrade later. If upgrade wins, user message's append + probes fresh `currentRunId` (v2), uses it. +- **Session expiry mid-conversation** — `.in/append` and `end-and-continue` + reject after `expiresAt`. Existing run keeps running until idle, then + exits. Frontend sees a 400. +- **Concurrent `POST /sessions`** — unique constraint on + `(env, externalId)`, idempotent upsert returns existing row + ensure-runs. + +## Tests + +### Unit + +- `ensureRunForSession`: + - Happy path (no contention) + - Concurrent contention (two callers, one wins, loser reuses winner's + run) + - Pathological recursion (winner's run dies before loser re-reads) + - Trigger failure (caller's responsibility to surface) +- `POST /sessions` route: + - Idempotent upsert (same externalId → same row, fresh PAT) + - Auth: secret key path, JWT path with valid scope, JWT path with wrong + task scope (403), JWT replay (consumed token rejected) + - First run triggered, runId in response +- `POST /sessions/:id/in/append`: + - Probe path: alive run, terminal run, null currentRunId + - Probe + trigger: ensure new run before append + - Closed session 400 + - Expired session 400 +- `POST /sessions/:id/end-and-continue`: + - Auth: only callable from the current run + - Optimistic locking: stale currentRunId loses gracefully + +### Integration + +- chat.test.ts rewrite around the new transport surface (no `runId`, + no `triggerNewRun`) +- mock-chat-agent harness updates: install `__setSessionCreateImplForTests` + to also stub the first-run trigger (the create + trigger is now atomic + on the server, so the test harness needs to surface a fake runId) + +### Smoke (manual via Chrome DevTools) + +Same checklist as the previous branch's smoke test, plus: + +- Cancel run via dashboard → next user message triggers fresh run + automatically (no longer a gap) +- Deploy a new agent version mid-conversation → existing run requests + upgrade, exits, new run continues seamlessly (transport sees no + interruption beyond a possible extra TTFB) + +## Verification plan + +Per-package: + +``` +pnpm run typecheck --filter webapp # apps + internal pkgs +pnpm run typecheck --filter @internal/run-engine +pnpm run build --filter @trigger.dev/sdk # public package +pnpm run build --filter @trigger.dev/core # public package +pnpm run test --filter webapp -- sessionRunManager +pnpm run test --filter @trigger.dev/sdk -- chat +``` + +End-to-end via the playground: + +1. ai-chat (chat.agent) — basic send + reply +2. ai-chat-session (custom agent) — basic send + reply +3. ai-chat-raw — basic send + reply +4. ai-chat-hydrated — basic send + reply +5. Mid-stream reload — SSE reconnect +6. Stop + follow-up — same run handles next turn +7. Cancel run + send message → new run triggered automatically (the gap + from previous branch's S4 — must pass cleanly here) +8. Deploy new version + send message → in-flight conversation upgrades + transparently +9. Cross-form addressing curl matrix — unchanged from previous branch + +## Rollout + +- Single feature branch off `main` (or off the previous chat-agent-sessions + branch once that lands). +- No flag, no shim. Hard cutover. Pre-release SDK version. +- Reference projects updated in the same PR so the smoke test path works. + +## Open questions + +1. **Should `end-and-continue` accept a custom `triggerConfig` override?** + Use case: agent wants to swap to a different task identifier (rare). + Probably defer — keep it strictly "trigger another run with the same + config" for v1. +2. **Should `triggerConfig` pin the deploy version?** If a customer + redeploys with a chat.agent contract change, in-flight sessions might + have payloads incompatible with the new version. Probably defer — + chat.agent contract is stable; signature-breaking changes are rare and + warrant explicit handling. +3. **`SessionRun` join table**: yes/no/defer? Defer to v1.1 unless a + concrete dashboard surface needs it. +4. **`getSnapshotStatus` cost on hot path** — measure before optimizing. + Redis snapshot read should be sub-ms; if it isn't, cache for 1-2s + per session. + +## Out of scope + +- Session-level retry policies (separate feature) +- Multi-run-per-session (parallel agents on one chat) — explicit + non-goal; one currentRunId by design +- Cross-environment sessions (a session in dev, run in prod) — not + considered +- Public `Session.requestRun()` for callers other than the running + agent itself — defer until a use case appears +- Webhook notifications on run swap — defer + +## Effort estimate + +- Schema + migration: 0.5 day +- `ensureRunForSession` service + tests: 1.5 days +- `POST /sessions` auth modes + idempotent upsert + first-run trigger: 1 day +- `.in/append` probe: 0.5 day +- `end-and-continue` route + agent runtime wiring: 1 day +- Transport rewrite + tests: 2.5 days +- chat-client rewrite + tests: 1 day +- chat.agent runtime cleanup: 0.5 day +- `chat.createStartSessionAction` + browser path: 1 day +- Reference project migration: 0.5 day +- Smoke test + bug-fix buffer: 1.5 days + +**~11 days** focused work. Plus design doc review and any architectural +back-and-forth — call it 2 weeks calendar. + +## Implementation order + +1. Schema + migration (gives the new columns; everything else builds on this) +2. `ensureRunForSession` service + unit tests (the load-bearing primitive) +3. `POST /sessions` route changes (creates a session that actually has a run) +4. `.in/append` probe path (so the server can self-heal between runs) +5. `end-and-continue` route + chat.agent runtime call (upgrade flow) +6. Transport rewrite (depends on all the server pieces) +7. chat-client rewrite (mirrors transport; cheap once that's done) +8. `chat.createStartSessionAction` + reference project migration +9. Smoke test + final bug fixes diff --git a/.claude/docs-plans/sessions-as-run-manager-docs.md b/.claude/docs-plans/sessions-as-run-manager-docs.md new file mode 100644 index 00000000000..4d8e858a876 --- /dev/null +++ b/.claude/docs-plans/sessions-as-run-manager-docs.md @@ -0,0 +1,366 @@ +# Docs update plan: Sessions-as-run-manager + +Companion to commits `7a48c1e6` (ai-chat) and `427541c2` (sessions server). Captures every doc page that needs to change, what's getting removed, and an upgrade guide for prerelease users. + +## Architectural summary (the diff readers should internalize) + +Pre-migration mental model: Sessions and chat.agent were two separate primitives. Sessions had its own create/list/close API; chat.agent rolled its own run-scoped streams. The two coexisted but didn't share machinery — chat.agent's wire path (run streams) was distinct from Sessions' wire path (`.in` / `.out` channels). + +Post-migration mental model: **Sessions is the run manager.** A Session row is task-bound (`taskIdentifier` + `triggerConfig` are required), it owns its current run via `currentRunId` (optimistic-claim), and it tracks every run it ever triggered in a `SessionRun` audit table. chat.agent is now just a particular kind of task you bind a Session to. The standalone "create a Session, then trigger something against it" path is gone — `sessions.start({...})` atomically creates the row and triggers the first run. + +Wire-level, the transport now talks to one set of routes (`/realtime/v1/sessions/:s/...` and `/api/v1/sessions/:s/...`); the per-run-stream code path is dead for chat. + +## Standalone Sessions docs: REMOVE + +`docs/sessions/` was written for the standalone-Session model. With sessions now task-bound, every page in that directory is incorrect: + +- `sessions/overview.mdx` — describes a generic session-as-bidirectional-channel primitive. Standalone create/list/close as the entry point. +- `sessions/quick-start.mdx` — `sessions.create({type, externalId})` then trigger something. Pattern no longer exists. +- `sessions/channels.mdx` — `.in` / `.out` documented from the standalone-session perspective. +- `sessions/reference.mdx` — API surface for the standalone primitive. + +**Action:** +1. Delete all four files: `docs/sessions/{overview,quick-start,channels,reference}.mdx`. +2. Remove the entire `Sessions` group from `docs/docs.json` under the `AI` group: + ```json + { + "group": "Sessions", + "pages": ["sessions/overview", "sessions/quick-start", "sessions/channels", "sessions/reference"] + } + ``` +3. Don't redirect — the URLs were never widely shared (this was alpha-tier surface). If we add Sessions docs back later, we can decide redirect-vs-fresh-slug then. + +We'll re-introduce Sessions docs once the primitive is stable and we have a non-chat.agent customer flow to document. + +## ai-chat docs: UPDATE + +Pages listed in the order they appear in `docs.json`. Each entry calls out the specific stale claims and what to replace. + +### `ai-chat/overview.mdx` +- Replace any line that says chat.agent runs on per-run streams or that the transport mints run-scoped tokens. +- Add one paragraph on the underlying primitive: chat.agent is bound to a Session that owns its runs. Customer-facing surface unchanged. +- If there's a "how it works" diagram, update arrows: browser → server action → `chat.createStartSessionAction` → Session row + first run + session PAT → browser → `.in/append` + `.out` SSE. + +### `ai-chat/changelog.mdx` +- Add an entry for the migration: "Sessions-as-run-manager — chat.agent now runs on top of a durable Session row that owns its runs. Public surface unchanged. See upgrade guide." + +### `ai-chat/quick-start.mdx` +- The transport snippet is the highest-value example in the docs. It must show the new shape: + ```ts + const transport = useTriggerChatTransport({ + task: "my-agent", + accessToken: ({ chatId }) => mintAccessToken(chatId), + startSession: ({ chatId, taskId, clientData }) => + startChatSession({ chatId, taskId, clientData }), + }); + ``` +- Server actions page should show `chat.createStartSessionAction("my-agent")` and `auth.createPublicToken({scopes: {sessions: chatId}})`. +- Drop any mention of `getStartToken` and `auth.createTriggerPublicToken` for the chat path. + +### `ai-chat/backend.mdx` +- The `chat.agent({...})` shape itself is unchanged — leave the `run`, `onPreload`, `onTurnStart`, `onTurnComplete` callbacks alone. +- Add a section on `chat.createStartSessionAction(taskId, options?)`. This is the canonical server-side entry point now. Show: + - Default `triggerConfig.basePayload`: `{messages: [], trigger: "preload"}` baked in. Customer overrides via `options.triggerConfig`. + - Idempotent on `(env, externalId)`. Concurrent calls for the same chatId converge. + - Returns `{sessionId, runId, publicAccessToken}`. +- Update `chat.requestUpgrade()` description: it now calls `endAndContinueSession` server-side, which atomically swaps `Session.currentRunId` to a new run. Browser keeps streaming across the swap. + +### `ai-chat/frontend.mdx` +- This is where most of the transport API lives. Rewrite around the two callbacks: + - `accessToken: ({chatId}) => string` — pure refresh, called on 401/403. + - `startSession?: ({chatId, taskId, clientData}) => {publicAccessToken}` — wraps the customer's server action, called on `transport.preload(chatId)` and lazy first `sendMessage`. +- Show the typed `clientData` flow: `useTriggerChatTransport` infers `clientData` from `withClientData`, threads it into `startSession`'s params, and merges into per-turn `metadata`. +- Drop `getStartToken` documentation entirely. +- `transport.preload(chatId)` no longer takes per-call options. If the customer needs dynamic per-call config they capture it in their server action via closure (typically over a ref for live values like the playground's `clientDataJsonRef`). +- Persistable `ChatSession`: `{publicAccessToken, lastEventId?}`. `runId` is gone. + +### `ai-chat/server-chat.mdx` +- `AgentChat` (server-side chat client) — same shape, but the `session` prop now takes `{lastEventId?}` only. +- `onTriggered({runId, chatId})` callback is still useful for telemetry / dashboard linking — the `runId` is the *current* run, not the only run. Note that across turns the runId may change (continuation runs after idle, upgrade runs, etc.). + +### `ai-chat/types.mdx` +- `ChatSession` — drop `runId`, drop `sessionId`. Just `{publicAccessToken, lastEventId?}`. +- `StartSessionParams`, `StartSessionResult` — new public types. +- `AccessTokenParams` — narrowed to `{chatId}` only (no metadata threading). +- Remove `GetStartTokenParams` from the type table. + +### `ai-chat/features.mdx` +- Audit for any mention of run-scoped streams, `CHAT_STREAM_KEY`, `CHAT_MESSAGES_STREAM_ID`, `CHAT_STOP_STREAM_ID`. All gone. +- Add: cross-form addressing on the wire (a session-scoped JWT minted for either `externalId` or `friendlyId` form authorizes either URL form). +- Add: SessionRun audit log — every run a chat session has triggered is recorded, queryable via the dashboard. + +### `ai-chat/compaction.mdx` +- Should be untouched (compaction lives inside `chat.agent`'s turn loop, doesn't depend on the wire model). + +### `ai-chat/pending-messages.mdx` +- Should be untouched (steering messages flow through `.in.append` regardless). + +### `ai-chat/background-injection.mdx` +- Same — injection happens inside the run, the run's wire path swap doesn't affect it. + +### `ai-chat/error-handling.mdx` +- Add: errors from `startSession` callback. The customer's server action can fail (auth check, DB write). Surface via `onSessionChange(chatId, null)` or via the customer's own try/catch in their callback. +- Replace any 401/403 retry logic that mentions `getStartToken` — it's `accessToken` now. + +### `ai-chat/mcp.mdx` +- Audit for `getStartToken` mentions in MCP tool examples. + +### `ai-chat/testing.mdx` +- The `mock-chat-agent` test harness moved to `setupSessionStartImplForTests` / similar — verify and update examples. +- Show how to mock `startSession` in unit tests (it's a fetch-mock or vi.fn returning `{publicAccessToken}`). + +### `ai-chat/client-protocol.mdx` +- The wire-level protocol page. Replace any `/realtime/v1/streams/{runId}/chat` URLs with `/realtime/v1/sessions/{chatId}/{io}`. +- Document the chunk shape on `.in`: tagged union — `{kind: "message", payload}` for user turns, `{kind: "stop"}` for stop signals, `{kind: "action", name, payload}` for typed actions. +- Document `.out` chunks: `UIMessageChunk`s interleaved with `trigger:turn-complete`, `trigger:upgrade-required` control markers. +- Cross-form addressing on session-scoped PATs. + +### `ai-chat/reference.mdx` +- Public API surface tables. `TriggerChatTransportOptions` — drop `getStartToken`, `triggerConfig`, `triggerOptions`; add `startSession`. +- `chat.createStartSessionAction(taskId, options?)` — full signature. +- `chat.requestUpgrade()` — keep, but note the new server-orchestrated swap behaviour. + +### `ai-chat/patterns/version-upgrades.mdx` +- This page is essentially about `chat.requestUpgrade()`. Update to explain the new mechanism: + - Old: agent emitted `trigger:upgrade-required` chunk, transport consumed it, transport triggered a new run from the browser side. + - New: agent calls `endAndContinueSession` (server-to-server), webapp atomically swaps `Session.currentRunId` to a freshly-triggered run, transport's existing SSE keeps streaming on the same session — no transport-side swap. +- Add: `SessionRun` audit row with `reason: "upgrade"`. + +### `ai-chat/patterns/sub-agents.mdx` +- Audit for any session.create / sub-agent-as-session-creator patterns. Sub-agents now get their session via the parent's task trigger (or by calling `sessions.start({ ... })` themselves with a different taskIdentifier). + +### `ai-chat/patterns/database-persistence.mdx` +- The reference app's `ChatSession` schema is now simpler: `{id, publicAccessToken, lastEventId?}`. Drop `runId`/`sessionId` columns from any example schemas. +- The persistence pattern itself is unchanged: persist the PAT + lastEventId, hydrate on page load via `sessions: { [chatId]: ... }` on the transport. + +### `ai-chat/patterns/branching-conversations.mdx` +- Should be mostly unchanged. Branching is a customer-side concern (multiple chatIds, each one its own session). + +### `ai-chat/patterns/code-sandbox.mdx` +- Audit for stale references. Probably fine. + +### `ai-chat/patterns/human-in-the-loop.mdx` +- Should be unchanged. + +### `ai-chat/patterns/skills.mdx` +- Should be unchanged. + +## NEW page: upgrade guide for chat.agent prerelease users + +Filename: `docs/ai-chat/upgrade-guide.mdx` (or `migration-from-prerelease.mdx` — pick whichever fits the docs style). Add to `docs.json` near the top of the AI Chat group, between `overview` and `quick-start`. + +Contents: + +```mdx +--- +title: "Upgrade guide: prerelease → Sessions-as-run-manager" +description: "Migrating chat.agent code from the prerelease API to the Sessions-as-run-manager release." +--- + +# Upgrade guide + +This guide is for customers who tried `chat.agent` during the prerelease period +(any `@trigger.dev/sdk` build before vX.Y.Z). The public surface is largely +unchanged — `chat.agent({...})`, `useTriggerChatTransport`, `chat.store` / +`chat.defer` / `chat.history`, `AgentChat` — but the transport callbacks and a +few server-side helpers were renamed. + +## TL;DR + +- **`getStartToken` is gone.** Replace with `startSession`, a server-action + callback that returns `{publicAccessToken}`. +- **`chat.createStartSessionAction(taskId, options?)` is the canonical + server-side entry point.** Replaces ad-hoc `auth.createTriggerPublicToken` + + manual session create. +- **`ChatSession` persistable shape changed.** Drop the `runId` field; + store only `{publicAccessToken, lastEventId?}`. +- **`transport.preload(chatId)` no longer takes per-call options.** + Trigger config (machine, idleTimeoutInSeconds, tags) lives server-side in + `chat.createStartSessionAction(taskId, options)`. +- **Wire URLs changed.** Anything that hit + `/realtime/v1/streams/{runId}/chat` directly should use + `/realtime/v1/sessions/{chatId}/out` (subscribe) or + `/realtime/v1/sessions/{chatId}/in/append` (send). + +## Transport: replace `getStartToken` with `startSession` + +### Before + +```ts +const transport = useTriggerChatTransport({ + task: "my-agent", + accessToken: async ({ chatId }) => mintToken(chatId), + getStartToken: async ({ taskId }) => mintTriggerToken(taskId), + triggerConfig: { basePayload: { /* ... */ } }, + triggerOptions: { tags: [...], machine: "small-1x" }, +}); +``` + +The browser called `auth.createTriggerPublicToken(taskId)` server-side to get +a one-shot trigger JWT, then `POST /api/v1/sessions` from the browser. + +### After + +```ts +const transport = useTriggerChatTransport({ + task: "my-agent", + accessToken: ({ chatId }) => mintAccessToken(chatId), + startSession: ({ chatId, taskId, clientData }) => + startChatSession({ chatId, taskId, clientData }), +}); +``` + +Where `startChatSession` is a server action wrapping +`chat.createStartSessionAction`: + +```ts +"use server"; +import { chat } from "@trigger.dev/sdk/ai"; + +export const startChatSession = chat.createStartSessionAction("my-agent", { + triggerConfig: { + machine: "small-1x", + tags: ["my-tag"], + }, +}); +``` + +The browser never holds a `trigger:tasks:{taskId}` JWT now. All session +creation goes through the customer's server, where authorization decisions +live alongside the customer's own DB writes. + +## Server actions: replace ad-hoc helpers with `chat.createStartSessionAction` + +### Before + +```ts +"use server"; +import { auth, sessions } from "@trigger.dev/sdk"; + +export async function startChatSession({ chatId, taskId }) { + const session = await sessions.create({ + type: "chat.agent", + externalId: chatId, + }); + // ... separately trigger the agent task ... + const publicAccessToken = await auth.createPublicToken({ + scopes: { read: { sessions: chatId }, write: { sessions: chatId } }, + }); + return { publicAccessToken }; +} +``` + +### After + +```ts +"use server"; +import { chat } from "@trigger.dev/sdk/ai"; + +export const startChatSession = chat.createStartSessionAction("my-agent"); +``` + +The new helper handles session creation + first-run trigger + PAT mint +atomically. It's idempotent on `(env, externalId)` — concurrent calls for the +same `chatId` converge to the same session. + +## `ChatSession` shape: drop `runId` + +Persistable session state is now just the PAT + last event ID: + +```ts +// before +type ChatSession = { runId: string; publicAccessToken: string; lastEventId?: string }; + +// after +type ChatSession = { publicAccessToken: string; lastEventId?: string }; +``` + +If your DB schema has a `runId` column on a session-state table, drop it (or +keep it for telemetry — but the transport doesn't read it). The current run +ID is server-side state on the Session row; the transport doesn't need to +know it. + +## `clientData`: typed and threaded automatically + +If your agent uses `chat.agent(...).withClientData({schema})`, the transport +infers the `clientData` type from `useTriggerChatTransport` +and threads it through `startSession`'s params. Set it once on the +transport: + +```ts +useTriggerChatTransport({ + // ... + clientData: { userId: currentUser.id, plan: currentUser.plan }, +}); +``` + +The same value also merges into per-turn `metadata` on the wire, and your +`startSession` callback receives it as `params.clientData`. Pass through to +`chat.createStartSessionAction` via `triggerConfig.basePayload.metadata` and +the agent's first run sees it in `payload.metadata`. + +## `chat.requestUpgrade()`: server-orchestrated now + +The behaviour didn't change from the customer's perspective — call +`chat.requestUpgrade()` inside `onTurnStart` / `onValidateMessages` and the +current run will exit so the next message starts on the latest version. + +What changed under the hood: + +- **Before:** the agent emitted a `trigger:upgrade-required` chunk on + `.out`, the transport consumed it browser-side and triggered a new run. +- **After:** the agent calls `endAndContinueSession` server-to-server, the + webapp triggers a new run and atomically swaps `Session.currentRunId`, + the browser's existing SSE subscription keeps receiving chunks across + the swap. Faster handoff, no browser-side bookkeeping. + +The `SessionRun` audit table records every run, including upgrade-driven +ones (with `reason: "upgrade"`). + +## Going to URLs directly? + +Anyone hitting raw URLs (instead of going through the SDK) should switch: + +| Before | After | +|---|---| +| `/realtime/v1/streams/{runId}/chat` (subscribe) | `/realtime/v1/sessions/{chatId}/out` | +| `/realtime/v1/streams/{runId}/{target}/chat-messages/append` | `/realtime/v1/sessions/{chatId}/in/append` (`{kind: "message", payload}` body) | +| `/realtime/v1/streams/{runId}/{target}/chat-stop/append` | `/realtime/v1/sessions/{chatId}/in/append` (`{kind: "stop"}` body) | + +The session-scoped PAT (`read:sessions:{chatId} + write:sessions:{chatId}`) +authorizes both the `externalId` form (e.g. `/sessions/my-chat-id/out`) +and the `friendlyId` form (e.g. `/sessions/session_abc.../out`). + +## Things that didn't change + +- `chat.agent({...})` definition shape and all callbacks. +- `chat.store` / `chat.defer` / `chat.history` APIs. +- `AgentChat` (server-side chat client) — same constructor, same methods. +- `useTriggerChatTransport`'s React semantics (created once, kept in a ref, + callbacks updated via `setOnSessionChange` / `setClientData` under the hood). +- Multi-tab coordination, pending-messages / steering, background injection. +- Per-turn `metadata` flowing through `sendMessage({ text }, { metadata })`. +``` + +## Other doc surfaces touched + +- `docs/ai/prompts.mdx` — only mentions `chat.agent` in passing. Audit but probably no change. +- `docs/realtime/backend/streams.mdx`, `docs/realtime/backend/input-streams.mdx` — these are the older streams API docs. Verify they don't reference `CHAT_STREAM_KEY` or `CHAT_MESSAGES_STREAM_ID` (those constants were removed). +- `docs/mcp-tools.mdx` — likely mentions the chat MCP tools. Audit for `getStartToken`-shaped examples. +- `docs/guides/example-projects/anchor-browser-web-scraper.mdx` — example project. Likely uses `chat.agent`. Audit. +- `docs/tasks/schemaTask.mdx` — only matched on the term "session" probably. Audit. + +## Update sequence + +Suggested order to minimise stale-state windows for readers: + +1. **Add the upgrade guide** (`ai-chat/upgrade-guide.mdx`) and its nav entry. This is the most-needed doc and stands alone from the rest. +2. **Update transport-shape pages** in this order: `quick-start` → `frontend` → `backend` → `server-chat` → `types` → `reference`. They all show the same callback shape; readers cross-reference between them, so they should ship together. +3. **Update peripheral pages**: `overview`, `changelog`, `client-protocol`, `error-handling`, `testing`, `features`, patterns. +4. **Remove `docs/sessions/`** + nav group last. Until step 2 lands the standalone Sessions docs are still less misleading than half-stale chat.agent docs. + +## Out of scope for this pass + +- Re-adding standalone Sessions docs (deferred until the primitive is stable for non-chat use). +- Diagrams / illustrations — text-first pass; designer can layer visuals after. +- Sample customer projects — the `references/ai-chat` reference repo is the in-source example; if marketing wants a polished standalone sample, that's a separate effort. diff --git a/.claude/review-guides/chat-agent-sessions-row-agnostic.md b/.claude/review-guides/chat-agent-sessions-row-agnostic.md new file mode 100644 index 00000000000..7fb9851f308 --- /dev/null +++ b/.claude/review-guides/chat-agent-sessions-row-agnostic.md @@ -0,0 +1,287 @@ +# Review guide — chat.agent on Sessions, row-agnostic addressing + +Scope: the 12 uncommitted files. **No new behaviour beyond the public surface +already on this branch** — this is plumbing cleanup that: + +1. Eliminates the transport's session-creation step +2. Makes `chatId` the universal addressing string everywhere +3. Makes the server-side stream/append/wait routes row-agnostic + +## The two design moves + +**Move 1 — agent owns session lifecycle.** `chat.agent` and +`chat.customAgent` upsert the backing `Session` row at bind, fire-and-forget, +keyed on `externalId = payload.chatId`. The transport, server-side +`AgentChat`, and `chat.createTriggerAction` no longer create sessions at all. +Browsers cannot mint sessions either (`POST /api/v1/sessions` is now +secret-key-only). One owner, one path. + +**Move 2 — `chatId` is the only address.** The transport, server-side +`AgentChat`, JWT scopes, and S2 stream paths all use `chatId` directly. The +Session's friendlyId is informational. To make this safe, the three stream +routes (`.in/.out` PUT, GET, POST append, plus the run-engine `wait` +endpoint) became "row-optional" and derive a *canonical addressing key* +(`row.externalId ?? row.friendlyId`, fallback to the URL param when the row +hasn't been upserted yet). Same canonical key is used to build the S2 stream +path, the waitpoint cache key, and the JWT resource set — so any caller +addressing by either form converges on the same physical stream. + +Together these remove an entire class of "did the row land yet?" races. The +transport can subscribe to `/sessions/{chatId}/out` before the agent boots, +the agent's `void sessions.create({externalId: chatId})` lands a moment +later, and any earlier reads/writes are already on the right S2 key. + +--- + +## Read in this order + +### 1. `apps/webapp/app/services/realtime/sessions.server.ts` (+34 lines) + +The new primitive. Two helpers: + +- `isSessionFriendlyIdForm(value)` — `value.startsWith("session_")`. Used to + decide whether a missing row is a hard 404 (opaque friendlyId) or a soft + "row will land later" (externalId form). +- `canonicalSessionAddressingKey(row, paramSession)` — `row.externalId ?? + row.friendlyId` if the row exists, else `paramSession`. **This is the load- + bearing function.** Read its docstring. + +**Question to ask:** can two callers addressing the "same" session ever get +different canonical keys? Only if the row exists for one and not the other, +*and* the URL forms differ — but in that case the row-less caller used the +externalId form (friendlyId-form would have 404'd earlier), and the row-ful +caller computes `row.externalId ?? row.friendlyId`. If the row's externalId +matches the URL, they converge. If it doesn't, there's no row to find by +that string anyway. The interesting edge is "row exists with no externalId", +addressed via friendlyId — both sides read `row.friendlyId`. ✓ + +### 2. `apps/webapp/app/routes/realtime.v1.sessions.$session.$io.ts` (+47/-12) + +PUT initialize + GET subscribe (SSE). Both use the helper. The interesting +part is the loader's `findResource` + `authorization.resource`: + +```ts +findResource: async (params, auth) => { + const row = await resolveSessionByIdOrExternalId(...); + if (!row && isSessionFriendlyIdForm(params.session)) return undefined; // 404 + return { row, addressingKey: canonicalSessionAddressingKey(row, params.session) }; +}, +authorization: { + resource: ({ row, addressingKey }) => { + const ids = new Set([addressingKey]); + if (row) { + ids.add(row.friendlyId); + if (row.externalId) ids.add(row.externalId); + } + return { sessions: [...ids] }; + }, + superScopes: ["read:sessions", "read:all", "admin"], +}, +``` + +**Why three IDs in the resource set?** `checkAuthorization` is "any-match" +across the resource values. We want a JWT scoped to *either* form to +authorize *either* URL form. Smoke test verified the 4-cell matrix passes. + +**The PUT path** (action handler) is simpler — it just resolves the row, +builds an addressing key, and hands it to `initializeSessionStream`. Worth +noting the `closedAt` check is now `maybeSession?.closedAt` — no row means +no closedAt to enforce. + +### 3. `apps/webapp/app/routes/realtime.v1.sessions.$session.$io.append.ts` (+22/-13) + +POST append (browser writes a record to `.in` or server writes to `.out`). +Same row-optional pattern. Both the S2 append and the waitpoint drain use +`addressingKey`. + +**Question to ask:** what fires the waitpoint? An agent's +`session.in.wait()` registers a waitpoint keyed on `(addressingKey, io)` via +the wait endpoint (file 4). The append handler drains by the *same* key — +even if the agent registered with externalId form and the transport +appended via friendlyId form, both compute the same canonical key, so they +converge. ✓ + +### 4. `apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts` (+18/-13) + +The agent's `.in.wait()` endpoint. Run-engine creates the waitpoint, then +registers it in Redis under `(addressingKey, io)`. The race-check that runs +right after creation reads from S2 by the same key. Three call sites — +`addSessionStreamWaitpoint`, `readSessionStreamRecords`, +`removeSessionStreamWaitpoint` — all consistent. + +### 5. `apps/webapp/app/routes/api.v1.sessions.ts` (+4/-2) + +**Security tightening.** Removed `allowJWT: true` and `corsStrategy: "all"` +from the `POST /api/v1/sessions` action — secret-key only now. + +**Question to ask:** was the JWT path actually used? Until this branch, the +transport called it via `ensureSession` (now deleted). After this branch, +nobody reaches it from the browser. `chat.createTriggerAction` (server +secret key) is the only browser-adjacent path. + +### 6. `packages/trigger-sdk/src/v3/ai.ts` (+62/-39) + +Two near-identical edits — one in `chatAgent`, one in `chatCustomAgent`. +Both bind on `payload.chatId` and fire-and-forget the upsert: + +```ts +locals.set(chatSessionHandleKey, sessions.open(payload.chatId)); +void sessions + .create({ type: "chat.agent", externalId: payload.chatId }) + .catch(() => { /* best effort */ }); +``` + +**Question to ask:** why `void`-and-`catch`? Awaiting the upsert would gate +the agent's bind on a network round-trip that doesn't unblock anything +user-visible — `.in/.out` routes are row-agnostic and the waitpoint cache +is keyed on the addressing string, not the row id. If the upsert genuinely +fails, the next bind retries the same idempotent call (`sessions.create` +upserts on `externalId`, so concurrent triggers on one chatId converge to +one row). The row matters for downstream metadata + listing, not for live +addressing. + +The PAT scope minting in `chatAgent` (two call sites — preload and +sendMessage) now uses `payload.chatId` for the `sessions:` resource. That +matches what the transport/AgentChat use as the JWT resource and what the +JWT's resource set in the loader includes. Cross-form addressing works +either way (smoke-tested), but using `chatId` keeps the chain tight. + +`createChatTriggerAction` is the most visibly trimmed: no pre-create, no +threading `sessionId` into payload, scope mint uses `chatId`. Return type +no longer carries `sessionId` — note `TriggerChatTaskResult.sessionId` was +already declared optional, so this isn't a public-API break. + +**Stale docstring to flag:** `chat.ts:59` and `chat.ts:112` still describe +PAT scopes as `read:sessions:{sessionId}` and +`write:sessions:{sessionId}`. Functionally either ID works (row lookup +canonicalises), but the doc text is now out of date — it should say +`{chatId}`. Worth a tidy-up before merge but not blocking. + +### 7. `packages/trigger-sdk/src/v3/chat.ts` (+63/-117) + +**The biggest mechanical edit.** Net -54 lines from deleting `ensureSession` +and untangling its callers. + +What disappeared: +- `private async ensureSession(chatId)` — gone +- The "lazy upsert from the browser if no triggerTask callback" branch in + `sendMessages` and `preload` — gone +- The "throw if neither path surfaced a sessionId" guard — gone +- All `state.sessionId` URL params replaced with `chatId` +- `subscribeToSessionStream`'s `chatId?` (optional) is now `chatId` (required) + +What stayed: +- `state.sessionId` in `ChatSessionState` — optional, informational +- The `restore from external storage` branch in the constructor still + hydrates `sessionId` if persisted, just doesn't *require* it +- `notifySessionChange` still surfaces `sessionId` if known + +**Question to ask:** does the transport ever still need the friendlyId? The +only place is the `onSessionChange` callback's payload (so consumers +persisting state can save it for later display). The transport itself never +puts it in a URL or a waitpoint key. + +The `sendMessages` path is worth re-reading: when state.runId is set, it +appends to `.in/append` and subscribes to `.out`. If the append fails with +a non-auth error, it falls through to triggering a new run (legacy "run is +dead" detection — unchanged from pre-Sessions, doesn't depend on +addressing). + +### 8. `packages/trigger-sdk/src/v3/chat-client.ts` (+34/-33) + +Server-side `AgentChat`. Mirrors the transport changes — every URL uses +`this.chatId`. `triggerNewRun` no longer pre-creates a session. `ChatSession` +and internal `SessionState` types now have optional `sessionId`. + +The shape of the diff is identical to the transport: delete the upsert, +swap addressing identifiers, optionalise the friendlyId. If you've read +`chat.ts` carefully, this one is mostly mechanical confirmation that both +client surfaces (browser transport + server-side AgentChat) speak the same +addressing protocol. + +### 9. Test infrastructure — `sessions.ts` (+18) + `mock-chat-agent.ts` (+25) + +`__setSessionCreateImplForTests` mirrors the existing +`__setSessionOpenImplForTests`. `mockChatAgent` installs a no-op create stub +returning a synthetic `CreatedSessionResponseBody` so the agent's bind-time +`void sessions.create(...)` doesn't try to hit a real API. Cleanup runs in +the same `.finally` as the open override. + +**Question to ask:** is the synthetic response shape correct? It mirrors +`CreatedSessionResponseBody` — `id`, `externalId`, `type`, `tags`, +`metadata`, `closedAt`, `closedReason`, `expiresAt`, `createdAt`, +`updatedAt`, `isCached`. Tests don't currently assert on this object, so +the bar is "doesn't crash + matches the type". Met. + +### 10. `packages/trigger-sdk/src/v3/chat.test.ts` (+13/-12) + +Three classes of test edits, all consequences: + +- Stream URL assertion: `chat-1` (the chatId) instead of + `session_streamurl` (the friendlyId) +- `renewRunAccessToken` callback: `sessionId: undefined` (was + `DEFAULT_SESSION_ID` because the mocked trigger doesn't surface it) +- Token resolve count: `1` (was `2` — second resolve was for `ensureSession`) +- One `onSessionChange` matchObject loses `sessionId` + +### 11. `apps/webapp/app/routes/_app.../playground/.../route.tsx` (1 line) + +`sessionId: string` → `sessionId?: string` in the playground sidebar prop +to track the transport type change. + +--- + +## Edge cases I checked, so you don't have to + +- **Cross-form JWT auth (curl matrix).** JWT scoped to externalId can call + externalId URL ✓ and friendlyId URL ✓. JWT scoped to friendlyId can call + externalId URL ✓ and friendlyId URL ✓. Smoke-tested. +- **Row materialises after subscribe.** Transport opens + `GET /sessions/{chatId}/out` before agent's bind upsert lands → 200 OK, + `addressingKey = chatId` (paramSession fallback). Once the row lands + with `externalId = chatId`, addressingKey resolves to the same value via + `row.externalId`. Same S2 key throughout. +- **Concurrent triggers on one chatId.** Two browser tabs trigger two runs + → two binds → two `sessions.create({externalId: chatId})` calls. Upsert + semantics: both return the same row. +- **Closed session enforcement.** Still enforced when a row exists. + `maybeSession?.closedAt` is null-safe; no row = no close-state to honour. +- **Agent run cancellation.** Frontend doesn't auto-detect — unchanged from + pre-Sessions; messages sit in S2 until the next trigger (the existing + run-PAT auth-error path is the only reaper). Out of scope for this branch. +- **Idle timeout in dev.** Runs stay `EXECUTING_WITH_WAITPOINTS` past the + configured idle because dev runs don't snapshot/restore; the in-process + idle clock advances locally without touching the row. Expected, not a + regression. + +## Things explicitly **not** in this branch + +- Run-state subscription on the transport side (the "run died, re-trigger + silently" UX gap) +- Session auto-close on agent exit (still client-driven by design) +- Any change to `Session` schema, `sessions.create` semantics, or + `chatAccessTokenTTL` +- Docstring updates for `read:sessions:{sessionId}` / `write:sessions:{sessionId}` + in `chat.ts:59` and `chat.ts:112` (functional but textually stale — + follow-up nit) + +--- + +## What I'd be ready to answer cold + +- Why fire-and-forget upsert (vs. `await`) in the agent's bind step +- Why the route's authorization resource set has three IDs (cross-form JWT + auth) +- Why `POST /api/v1/sessions` lost `allowJWT` (security tightening — no + caller needs it after the transport's `ensureSession` is gone) +- What converges two callers using different URL forms onto the same S2 + stream (`canonicalSessionAddressingKey`, identical computation on both + sides for any given row) +- What makes `sessions.create` race-safe under concurrent triggers + (`externalId` upsert) +- Why `state.sessionId` stayed on `ChatSessionState` at all (pure + informational, surfaced via `onSessionChange` for consumer persistence; + zero addressing role) +- Why the chat-client (server-side AgentChat) and chat (transport) edits + look near-identical (they implement the same client protocol against the + same row-agnostic routes) diff --git a/.claude/rules/package-installation.md b/.claude/rules/package-installation.md new file mode 100644 index 00000000000..310074823c5 --- /dev/null +++ b/.claude/rules/package-installation.md @@ -0,0 +1,22 @@ +--- +paths: + - "**/package.json" +--- + +# Installing Packages + +When adding a new dependency to any package.json in the monorepo: + +1. **Look up the latest version** on npm before adding: + ```bash + pnpm view version + ``` + If unsure which version to use (e.g. major version compatibility), confirm with the user. + +2. **Edit the package.json directly** — do NOT use `pnpm add` as it can cause issues in the monorepo. Add the dependency with the correct version range (typically `^x.y.z`). + +3. **Run `pnpm i` from the repo root** after editing to install and update the lockfile: + ```bash + pnpm i + ``` + Always run from the repo root, not from the package directory. diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000000..3f0bfbb869d --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"a0e063d3-034b-40fe-90d0-7a6aff597e26","pid":12327,"procStart":"Tue Apr 28 13:47:55 2026","acquiredAt":1777384775033} diff --git a/packages/cli-v3/src/build/buildWorker.ts b/packages/cli-v3/src/build/buildWorker.ts index b23b802f502..ddfd8dad8fa 100644 --- a/packages/cli-v3/src/build/buildWorker.ts +++ b/packages/cli-v3/src/build/buildWorker.ts @@ -1,6 +1,7 @@ import { ResolvedConfig } from "@trigger.dev/core/v3/build"; import { BuildManifest, BuildTarget } from "@trigger.dev/core/v3/schemas"; import { BundleResult, bundleWorker, createBuildManifestFromBundle } from "./bundle.js"; +import { bundleSkills } from "./bundleSkills.js"; import { createBuildContext, notifyExtensionOnBuildComplete, @@ -8,6 +9,8 @@ import { resolvePluginsForContext, } from "./extensions.js"; import { createExternalsBuildExtension } from "./externals.js"; +import { tmpdir } from "node:os"; +import { mkdtemp } from "node:fs/promises"; import { join, relative, sep } from "node:path"; import { generateContainerfile } from "../deploy/buildImage.js"; import { writeFile } from "node:fs/promises"; @@ -97,6 +100,29 @@ export async function buildWorker(options: BuildWorkerOptions) { envVars: options.envVars, }); + // Built-in skill bundler — discovers `ai.defineSkill` registrations + // via a local indexer run and copies each skill folder into + // `{destination}/.trigger/skills/{id}/` before Docker COPY picks up + // the bundle. First-class, not a build extension. + const skillsTmpDir = await mkdtemp(join(tmpdir(), "trigger-skills-")); + const skillsBuildManifestPath = join(skillsTmpDir, "build.json"); + try { + await writeFile(skillsBuildManifestPath, JSON.stringify(buildManifest)); + const skillsResult = await bundleSkills({ + buildManifest, + buildManifestPath: skillsBuildManifestPath, + workingDir: resolvedConfig.workingDir, + env: { + ...process.env, + ...(options.envVars ?? {}), + }, + logger: buildContext.logger, + }); + buildManifest = skillsResult.buildManifest; + } catch (err) { + logger.warn("Skill bundling failed; continuing without skills", err); + } + buildManifest = await notifyExtensionOnBuildComplete(buildContext, buildManifest); if (options.target !== "dev") { diff --git a/packages/cli-v3/src/build/bundleSkills.ts b/packages/cli-v3/src/build/bundleSkills.ts new file mode 100644 index 00000000000..65ad9834abe --- /dev/null +++ b/packages/cli-v3/src/build/bundleSkills.ts @@ -0,0 +1,135 @@ +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path"; +import type { BuildManifest, SkillManifest } from "@trigger.dev/core/v3/schemas"; +import { copyDirectoryRecursive } from "@trigger.dev/build/internal"; +import { indexWorkerManifest } from "../indexing/indexWorkerManifest.js"; +import { execOptionsForRuntime, type BuildLogger } from "@trigger.dev/core/v3/build"; + +export type BundleSkillsOptions = { + buildManifest: BuildManifest; + buildManifestPath: string; + workingDir: string; + env: Record; + logger: BuildLogger; +}; + +export type BundleSkillsResult = { + /** The input manifest, annotated with `skills` on return. */ + buildManifest: BuildManifest; + /** Discovered skills, in deterministic order. */ + skills: SkillManifest[]; +}; + +/** + * Built-in skill bundler — not an extension. Runs the indexer locally + * against the bundled worker output to discover `ai.defineSkill(...)` + * registrations, validates each skill's `SKILL.md`, and copies the + * folder into `{outputPath}/.trigger/skills/{id}/` so the deploy image + * picks it up via the existing Dockerfile `COPY`. + * + * No `trigger.config.ts` changes required — discovery is side-effect + * based, same mechanism as task/prompt registration. + */ +export async function bundleSkills( + options: BundleSkillsOptions +): Promise { + const { buildManifest, buildManifestPath, workingDir, env, logger } = options; + + let skills: SkillManifest[]; + try { + const workerManifest = await indexWorkerManifest({ + runtime: buildManifest.runtime, + indexWorkerPath: buildManifest.indexWorkerEntryPoint, + buildManifestPath, + nodeOptions: execOptionsForRuntime(buildManifest.runtime, buildManifest), + env, + cwd: workingDir, + otelHookInclude: buildManifest.otelImportHook?.include, + otelHookExclude: buildManifest.otelImportHook?.exclude, + handleStdout(data) { + logger.debug(`[bundleSkills] ${data}`); + }, + handleStderr(data) { + if (!data.includes("Debugger attached")) { + logger.debug(`[bundleSkills:stderr] ${data}`); + } + }, + }); + skills = workerManifest.skills ?? []; + } catch (err) { + // Skill discovery via the indexer is best-effort — if the user's + // bundle doesn't load cleanly here the downstream full indexer will + // surface the real error. Warn so the user sees what went wrong. + logger.warn( + `[bundleSkills] skill discovery failed, skipping skill bundling: ${(err as Error).message}` + ); + return { buildManifest, skills: [] }; + } + + if (skills.length === 0) { + return { buildManifest, skills: [] }; + } + + // Destination layout differs between dev and deploy: + // - Dev: the worker runs with cwd = workingDir, so skills must live at + // {workingDir}/.trigger/skills/{id}/ for skill.local() to find them. + // - Deploy: the Dockerfile COPY picks up everything under outputPath into + // /app, so we target {outputPath}/.trigger/skills/{id}/ and the + // container's cwd (/app) resolves correctly. + const destinationRoot = + buildManifest.target === "dev" + ? join(workingDir, ".trigger", "skills") + : join(buildManifest.outputPath, ".trigger", "skills"); + + for (const skill of skills) { + // Resolve the skill's source folder relative to the file that called + // `skills.define(...)`. Absolute paths are honored as-is. + const callerDir = skill.filePath + ? dirname(resolvePath(workingDir, skill.filePath)) + : workingDir; + const sourcePath = isAbsolute(skill.sourcePath) + ? skill.sourcePath + : resolvePath(callerDir, skill.sourcePath); + const skillMdPath = join(sourcePath, "SKILL.md"); + + let skillMd: string; + try { + skillMd = await readFile(skillMdPath, "utf8"); + } catch { + throw new Error( + `Skill "${skill.id}": SKILL.md not found at ${skillMdPath}. ` + + `Registered via ai.defineSkill({ id: "${skill.id}", path: "${skill.sourcePath}" }) ` + + `at ${skill.filePath}.` + ); + } + + if (!/^---\r?\n[\s\S]*?\r?\n---/.test(skillMd)) { + throw new Error( + `Skill "${skill.id}": SKILL.md at ${skillMdPath} is missing a frontmatter block.` + ); + } + if (!/\bname:\s*\S/.test(skillMd) || !/\bdescription:\s*\S/.test(skillMd)) { + throw new Error( + `Skill "${skill.id}": SKILL.md at ${skillMdPath} frontmatter must include both \`name\` and \`description\`.` + ); + } + + const skillDest = join(destinationRoot, skill.id); + logger.debug(`[bundleSkills] Copying ${sourcePath} → ${skillDest}`); + await copyDirectoryRecursive(sourcePath, skillDest); + } + + // Sort by id for deterministic manifest output + skills = [...skills].sort((a, b) => a.id.localeCompare(b.id)); + + // Content hash is derived from each SKILL.md's content for cache invalidation + // downstream (dashboard persistence in Phase 2). Not used in Phase 1. + void createHash; + void dirname; + + return { + buildManifest: { ...buildManifest, skills }, + skills, + }; +} diff --git a/packages/cli-v3/src/dev/devSession.ts b/packages/cli-v3/src/dev/devSession.ts index 482ebf6fc17..2d6645cd50c 100644 --- a/packages/cli-v3/src/dev/devSession.ts +++ b/packages/cli-v3/src/dev/devSession.ts @@ -9,6 +9,7 @@ import { logBuildFailure, logBuildWarnings, } from "../build/bundle.js"; +import { bundleSkills } from "../build/bundleSkills.js"; import { createBuildContext, notifyExtensionOnBuildComplete, @@ -118,6 +119,26 @@ export async function startDevSession({ bundle.metafile ); + // Built-in skill bundling — copies registered skill folders into + // `.trigger/skills/{id}/` so `skill.local()` works at dev runtime. + try { + const buildManifestPath = join( + workerDir?.path ?? destination.path, + "build.json" + ); + await writeJSONFile(buildManifestPath, buildManifest); + const skillsResult = await bundleSkills({ + buildManifest, + buildManifestPath, + workingDir: rawConfig.workingDir, + env: process.env, + logger: buildContext.logger, + }); + buildManifest = skillsResult.buildManifest; + } catch (err) { + logger.warn("Skill bundling failed during dev rebuild", err); + } + buildManifest = await notifyExtensionOnBuildComplete(buildContext, buildManifest); try { diff --git a/packages/cli-v3/src/dev/taskRunProcessPool.ts b/packages/cli-v3/src/dev/taskRunProcessPool.ts index 810be7acb43..414626e64cc 100644 --- a/packages/cli-v3/src/dev/taskRunProcessPool.ts +++ b/packages/cli-v3/src/dev/taskRunProcessPool.ts @@ -127,7 +127,11 @@ export class TaskRunProcessPool { return { taskRunProcess: newProcess, isReused: false }; } - async returnProcess(process: TaskRunProcess, version: string): Promise { + async returnProcess( + process: TaskRunProcess, + version: string, + options?: { forceKill?: boolean } + ): Promise { // Remove from busy processes for this version const busyProcesses = this.busyProcessesByVersion.get(version); if (busyProcesses) { @@ -141,6 +145,19 @@ export class TaskRunProcessPool { ); } + // `forceKill` skips the reuse heuristic and tears the process down. Used + // on outcomes that leave the process in a state we can't safely reuse + // (OOM in particular — production would get a fresh container, so local + // dev should match that). + if (options?.forceKill) { + logger.debug("[TaskRunProcessPool] Force-killing process", { + version, + pid: process.pid, + }); + await this.killProcess(process); + return; + } + if (this.shouldReuseProcess(process, version)) { const availableCount = this.availableProcessesByVersion.get(version)?.length || 0; const busyCount = this.busyProcessesByVersion.get(version)?.size || 0; diff --git a/packages/cli-v3/src/entryPoints/dev-index-worker.ts b/packages/cli-v3/src/entryPoints/dev-index-worker.ts index 53b95ad040a..3e48c70e42e 100644 --- a/packages/cli-v3/src/entryPoints/dev-index-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-index-worker.ts @@ -184,6 +184,7 @@ await sendMessageInCatalog( manifest: { tasks, prompts: convertPromptSchemasToJsonSchemas(resourceCatalog.listPromptManifests()), + skills: resourceCatalog.listSkillManifests(), queues: resourceCatalog.listQueueManifests(), configPath: buildManifest.configPath, runtime: buildManifest.runtime, diff --git a/packages/cli-v3/src/entryPoints/dev-run-controller.ts b/packages/cli-v3/src/entryPoints/dev-run-controller.ts index fe486677452..ce73da66fdc 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-controller.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-controller.ts @@ -2,6 +2,8 @@ import { CompleteRunAttemptResult, DequeuedMessage, IntervalService, + isManualOutOfMemoryError, + isOOMRunError, LogLevel, RunExecutionData, SuspendedProcessError, @@ -52,6 +54,12 @@ export class DevRunController { private readonly cwd?: string; private isCompletingRun = false; private isShuttingDown = false; + // Set when the current attempt's outcome means the worker process can't + // safely be reused (OOM in particular). Production gives every retry a + // fresh container; local dev's process pool needs the same on these + // outcomes or in-process state (e.g. session.in cursors) leaks across + // attempts and the OOM retry skips the message that triggered it. + private discardProcessOnReturn = false; private state: | { @@ -539,6 +547,13 @@ export class DevRunController { error: TaskRunProcess.parseExecuteError(error), } satisfies TaskRunFailedExecutionResult; + // Same OOM check as the success path: if the thrown error parses to + // an OOM, force-kill the process when it's eventually returned (via + // runFinished / stop) instead of recycling it. + if (isOOMRunError(completion.error) || isManualOutOfMemoryError(completion.error)) { + this.discardProcessOnReturn = true; + } + const completionResult = await this.httpClient.dev.completeRunAttempt( run.friendlyId, this.snapshotFriendlyId ?? snapshot.friendlyId, @@ -664,10 +679,22 @@ export class DevRunController { this.isCompletingRun = true; + // Detect OOM in the failure result so we can force-kill the worker + // instead of returning it to the pool. Mirrors the production behavior + // where OOM retry happens on a brand-new container. + if ( + !completion.ok && + (isOOMRunError(completion.error) || isManualOutOfMemoryError(completion.error)) + ) { + this.discardProcessOnReturn = true; + } + // Return process to pool instead of killing it try { const version = this.opts.worker.serverWorker?.version || "unknown"; - await this.opts.taskRunProcessPool.returnProcess(this.taskRunProcess, version); + await this.opts.taskRunProcessPool.returnProcess(this.taskRunProcess, version, { + forceKill: this.discardProcessOnReturn, + }); this.taskRunProcess = undefined; } catch (error) { logger.debug("Failed to return task run process to pool, submitting completion anyway", { @@ -820,7 +847,9 @@ export class DevRunController { if (this.taskRunProcess) { try { const version = this.opts.worker.serverWorker?.version || "unknown"; - await this.opts.taskRunProcessPool.returnProcess(this.taskRunProcess, version); + await this.opts.taskRunProcessPool.returnProcess(this.taskRunProcess, version, { + forceKill: this.discardProcessOnReturn, + }); this.taskRunProcess = undefined; } catch (error) { logger.debug("Failed to return task run process to pool during runFinished", { error }); @@ -854,7 +883,9 @@ export class DevRunController { if (this.taskRunProcess && !this.taskRunProcess.isBeingKilled) { try { const version = this.opts.worker.serverWorker?.version || "unknown"; - await this.opts.taskRunProcessPool.returnProcess(this.taskRunProcess, version); + await this.opts.taskRunProcessPool.returnProcess(this.taskRunProcess, version, { + forceKill: this.discardProcessOnReturn, + }); this.taskRunProcess = undefined; } catch (error) { logger.debug("Failed to return task run process to pool during stop", { error }); diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index 6ffc6afd29b..2ea48bea847 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -34,6 +34,7 @@ import { heartbeats, realtimeStreams, inputStreams, + sessionStreams, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; import { @@ -61,6 +62,7 @@ import { StandardHeartbeatsManager, StandardRealtimeStreamsManager, StandardInputStreamManager, + StandardSessionStreamManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; import { readFile } from "node:fs/promises"; @@ -186,6 +188,14 @@ const standardInputStreamManager = new StandardInputStreamManager( ); inputStreams.setGlobalManager(standardInputStreamManager); +const standardSessionStreamManager = new StandardSessionStreamManager( + apiClientManager.clientOrThrow(), + getEnvVar("TRIGGER_STREAM_URL", getEnvVar("TRIGGER_API_URL")) ?? "https://api.trigger.dev", + (getEnvVar("TRIGGER_STREAMS_DEBUG") === "1" || getEnvVar("TRIGGER_STREAMS_DEBUG") === "true") ?? + false +); +sessionStreams.setGlobalManager(standardSessionStreamManager); + const waitUntilTimeoutInMs = getNumberEnvVar("TRIGGER_WAIT_UNTIL_TIMEOUT_MS", 60_000); const waitUntilManager = new StandardWaitUntilManager(waitUntilTimeoutInMs); waitUntil.setGlobalManager(waitUntilManager); diff --git a/packages/cli-v3/src/entryPoints/managed-index-controller.ts b/packages/cli-v3/src/entryPoints/managed-index-controller.ts index 181d3d1093c..21aa3d829d2 100644 --- a/packages/cli-v3/src/entryPoints/managed-index-controller.ts +++ b/packages/cli-v3/src/entryPoints/managed-index-controller.ts @@ -104,6 +104,7 @@ async function indexDeployment({ packageVersion: buildManifest.packageVersion, cliPackageVersion: buildManifest.cliPackageVersion, tasks: workerManifest.tasks, + prompts: workerManifest.prompts, queues: workerManifest.queues, sourceFiles, runtime: workerManifest.runtime, diff --git a/packages/cli-v3/src/entryPoints/managed-index-worker.ts b/packages/cli-v3/src/entryPoints/managed-index-worker.ts index 644673537e3..1bf73b22621 100644 --- a/packages/cli-v3/src/entryPoints/managed-index-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-index-worker.ts @@ -180,6 +180,7 @@ await sendMessageInCatalog( manifest: { tasks, prompts: convertPromptSchemasToJsonSchemas(resourceCatalog.listPromptManifests()), + skills: resourceCatalog.listSkillManifests(), queues: resourceCatalog.listQueueManifests(), configPath: buildManifest.configPath, runtime: buildManifest.runtime, diff --git a/packages/cli-v3/src/entryPoints/managed-run-worker.ts b/packages/cli-v3/src/entryPoints/managed-run-worker.ts index 1d5d2a5f0d6..fdfcff38ef6 100644 --- a/packages/cli-v3/src/entryPoints/managed-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-run-worker.ts @@ -33,6 +33,7 @@ import { heartbeats, realtimeStreams, inputStreams, + sessionStreams, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; import { @@ -61,6 +62,7 @@ import { StandardHeartbeatsManager, StandardRealtimeStreamsManager, StandardInputStreamManager, + StandardSessionStreamManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; import { readFile } from "node:fs/promises"; @@ -159,6 +161,14 @@ const standardInputStreamManager = new StandardInputStreamManager( ); inputStreams.setGlobalManager(standardInputStreamManager); +const standardSessionStreamManager = new StandardSessionStreamManager( + apiClientManager.clientOrThrow(), + getEnvVar("TRIGGER_STREAM_URL", getEnvVar("TRIGGER_API_URL")) ?? "https://api.trigger.dev", + (getEnvVar("TRIGGER_STREAMS_DEBUG") === "1" || getEnvVar("TRIGGER_STREAMS_DEBUG") === "true") ?? + false +); +sessionStreams.setGlobalManager(standardSessionStreamManager); + const waitUntilTimeoutInMs = getNumberEnvVar("TRIGGER_WAIT_UNTIL_TIMEOUT_MS", 60_000); const waitUntilManager = new StandardWaitUntilManager(waitUntilTimeoutInMs); waitUntil.setGlobalManager(waitUntilManager); diff --git a/packages/cli-v3/src/mcp/config.ts b/packages/cli-v3/src/mcp/config.ts index d878d881e7d..0cef5cfaab4 100644 --- a/packages/cli-v3/src/mcp/config.ts +++ b/packages/cli-v3/src/mcp/config.ts @@ -213,4 +213,28 @@ export const toolsMetadata = { description: "Reactivate a previous dashboard-sourced version as the active override. Use get_prompt_versions to find dashboard versions that can be reactivated.", }, + list_agents: { + name: "list_agents", + title: "List Agents", + description: + "List all chat agents in the current worker. Agents are tasks created with chat.agent() or chat.customAgent(). Use start_agent_chat with an agent's slug to start a conversation.", + }, + start_agent_chat: { + name: "start_agent_chat", + title: "Start Agent Chat", + description: + "Start a conversation with a chat agent. Returns a chatId you can use with send_agent_message. Optionally preloads the agent so it initializes before the first message.", + }, + send_agent_message: { + name: "send_agent_message", + title: "Send Agent Message", + description: + "Send a message to an active agent chat and get the full response text back. Use the chatId from start_agent_chat. The agent remembers full context from previous messages in the same chat.", + }, + close_agent_chat: { + name: "close_agent_chat", + title: "Close Agent Chat", + description: + "Close an agent chat conversation. The agent exits its loop gracefully. Without this, the agent will close on its own when its idle timeout expires.", + }, }; diff --git a/packages/cli-v3/src/mcp/tools.ts b/packages/cli-v3/src/mcp/tools.ts index fd013b77644..f438989258d 100644 --- a/packages/cli-v3/src/mcp/tools.ts +++ b/packages/cli-v3/src/mcp/tools.ts @@ -29,6 +29,12 @@ import { removePromptOverrideTool, reactivatePromptOverrideTool, } from "./tools/prompts.js"; +import { listAgentsTool } from "./tools/agents.js"; +import { + startAgentChatTool, + sendAgentMessageTool, + closeAgentChatTool, +} from "./tools/agentChat.js"; import { respondWithError } from "./utils.js"; /** Tool names that perform write/mutating operations. */ @@ -43,6 +49,9 @@ const WRITE_TOOLS = new Set([ updatePromptOverrideTool.name, removePromptOverrideTool.name, reactivatePromptOverrideTool.name, + startAgentChatTool.name, + sendAgentMessageTool.name, + closeAgentChatTool.name, ]); export function registerTools(context: McpContext) { @@ -80,6 +89,10 @@ export function registerTools(context: McpContext) { updatePromptOverrideTool, removePromptOverrideTool, reactivatePromptOverrideTool, + listAgentsTool, + startAgentChatTool, + sendAgentMessageTool, + closeAgentChatTool, ]; for (const tool of tools) { diff --git a/packages/cli-v3/src/mcp/tools/agentChat.ts b/packages/cli-v3/src/mcp/tools/agentChat.ts new file mode 100644 index 00000000000..b06c1364285 --- /dev/null +++ b/packages/cli-v3/src/mcp/tools/agentChat.ts @@ -0,0 +1,481 @@ +import { z } from "zod"; +import { ApiClient, SSEStreamSubscription } from "@trigger.dev/core/v3"; +import { toolsMetadata } from "../config.js"; +import { CommonProjectsInput } from "../schemas.js"; +import { respondWithError, toolHandler } from "../utils.js"; + +// ─── In-memory chat sessions ────────────────────────────────────── + +type ChatMessage = { + id: string; + role: string; + parts: Array<{ type: string; [key: string]: unknown }>; +}; + +type ChatSession = { + /** `session_*` friendlyId — durable identity for the conversation. */ + sessionId: string; + /** Last-known live run id. Cleared when a run ends. */ + runId: string; + chatId: string; + agentId: string; + lastEventId?: string; + apiClient: ApiClient; + clientData?: Record; + /** Accumulated conversation messages for continuation payloads. */ + messages: ChatMessage[]; +}; + +const activeSessions = new Map(); + +// ─── ChatInputChunk serialization (mirrors TriggerChatTransport) ── + +type ChatInputChunk = + | { + kind: "message"; + payload: { + messages: ChatMessage[]; + chatId: string; + trigger: "submit-message" | "close" | "preload" | "regenerate-message" | "action"; + metadata?: unknown; + }; + } + | { kind: "stop"; message?: string }; + +function serializeInputChunk(chunk: ChatInputChunk): string { + return JSON.stringify(chunk); +} + +// ─── Start Agent Chat ───────────────────────────────────────────── + +const StartAgentChatInput = CommonProjectsInput.extend({ + agentId: z + .string() + .describe( + "The agent task ID to chat with. Use get_current_worker to see available agents." + ), + chatId: z + .string() + .describe("A unique conversation ID. Reuse to resume a conversation.") + .optional(), + clientData: z + .record(z.unknown()) + .describe("Client data to include with every message (e.g. userId, model).") + .optional(), + preload: z + .boolean() + .describe("Whether to preload the agent before the first message.") + .default(true), +}); + +export const startAgentChatTool = { + name: toolsMetadata.start_agent_chat.name, + title: toolsMetadata.start_agent_chat.title, + description: toolsMetadata.start_agent_chat.description, + inputSchema: StartAgentChatInput.shape, + handler: toolHandler(StartAgentChatInput.shape, async (input, { ctx }) => { + ctx.logger?.log("calling start_agent_chat", { input }); + + if (ctx.options.devOnly && input.environment !== "dev") { + return respondWithError( + `This MCP server is only available for the dev environment.` + ); + } + + const projectRef = await ctx.getProjectRef({ + projectRef: input.projectRef, + cwd: input.configPath, + }); + + const apiClient = await ctx.getApiClient({ + projectRef, + environment: input.environment, + scopes: [ + "write:tasks", + "read:runs", + "read:sessions", + "write:sessions", + ], + branch: input.branch, + }); + + const chatId = input.chatId ?? crypto.randomUUID(); + + // Check if session already exists + if (activeSessions.has(chatId)) { + return { + content: [ + { + type: "text", + text: `Chat ${chatId} is already active with agent ${activeSessions.get(chatId)!.agentId}. Use send_agent_message to continue the conversation.`, + }, + ], + }; + } + + // Create (or upsert) the backing Session. Idempotent via externalId — + // two MCP clients targeting the same chatId converge to the same row. + // Sessions are now task-bound: taskIdentifier + triggerConfig are + // required, and the server reuses them for every run scheduled by + // this session (initial + continuations after run termination). + // + // basePayload mirrors the browser-mediated `chat.createStartSessionAction` + // shape so the auto-triggered first run hits `onPreload` (not + // `onChatStart` with `preloaded: true`). Without `trigger: "preload"` + // + `messages: []`, the agent runtime bypasses both lifecycle hooks + // and `onTurnStart`'s DB write fails with "No record found". + // + // POST /api/v1/sessions auto-triggers the first run and returns its + // runId, so we don't need a separate triggerTask call. The `preload` + // flag on this MCP tool is kept as a no-op signal (true=default) for + // backwards compat — a Session is always created with a live run now. + const session = await apiClient.createSession({ + type: "chat.agent", + externalId: chatId, + taskIdentifier: input.agentId, + triggerConfig: { + basePayload: { + messages: [], + trigger: "preload", + chatId, + ...(input.clientData ? { metadata: input.clientData } : {}), + }, + tags: [`chat:${chatId}`], + }, + }); + + activeSessions.set(chatId, { + sessionId: session.id, + runId: session.runId, + chatId, + agentId: input.agentId, + apiClient, + clientData: input.clientData, + messages: [], + }); + + return { + content: [ + { + type: "text", + text: [ + `Agent chat started${input.preload ? " and preloaded" : ""}.`, + `- Chat ID: ${chatId}`, + `- Session ID: ${session.id}`, + `- Agent: ${input.agentId}`, + `- Run ID: ${session.runId}`, + ``, + `Use send_agent_message with chatId "${chatId}" to send messages.`, + ].join("\n"), + }, + ], + }; + }), +}; + +// ─── Send Agent Message ─────────────────────────────────────────── + +const SendAgentMessageInput = z.object({ + chatId: z.string().describe("The chat ID from start_agent_chat."), + message: z.string().describe("The message to send to the agent."), +}); + +export const sendAgentMessageTool = { + name: toolsMetadata.send_agent_message.name, + title: toolsMetadata.send_agent_message.title, + description: toolsMetadata.send_agent_message.description, + inputSchema: SendAgentMessageInput.shape, + handler: toolHandler(SendAgentMessageInput.shape, async (input, { ctx }) => { + ctx.logger?.log("calling send_agent_message", { input }); + + const session = activeSessions.get(input.chatId); + if (!session) { + return respondWithError( + `No active chat with ID "${input.chatId}". Use start_agent_chat first.` + ); + } + + const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const userMessage: ChatMessage = { + id: msgId, role: "user", parts: [{ type: "text", text: input.message }], + }; + + // Track the outgoing user message + session.messages.push(userMessage); + + const wirePayload = { + messages: [userMessage], + chatId: session.chatId, + trigger: "submit-message" as const, + metadata: session.clientData, + }; + + // If we have an active run, send via session.in. If that fails + // (run ended, token expired, etc.) fall back to triggering a new + // run on the same session with the full history. + if (session.runId) { + try { + await session.apiClient.appendToSessionStream( + session.sessionId, + "in", + serializeInputChunk({ kind: "message", payload: wirePayload }) + ); + } catch (sendErr: any) { + const result = await session.apiClient.triggerTask(session.agentId, { + payload: { + messages: session.messages, + chatId: session.chatId, + sessionId: session.sessionId, + trigger: "submit-message", + metadata: session.clientData, + continuation: true, + previousRunId: session.runId, + }, + options: { + payloadType: "application/json", + tags: [`chat:${session.chatId}`], + }, + }); + session.runId = result.id; + session.lastEventId = undefined; + } + } else { + // No run yet — trigger one (agent opens the session on startup). + const result = await session.apiClient.triggerTask(session.agentId, { + payload: { + ...wirePayload, + sessionId: session.sessionId, + }, + options: { + payloadType: "application/json", + tags: [`chat:${session.chatId}`], + }, + }); + session.runId = result.id; + } + + // Subscribe to the response stream and collect the full text + const { text, toolCalls, assistantMessage } = await collectAgentResponse(session); + + // Track the assistant response for continuation payloads + session.messages.push(assistantMessage); + + const formatted = formatAssistantParts(assistantMessage.parts); + const footer = `\n\n---\nRun: ${session.runId}`; + + return { + content: [{ type: "text", text: formatted + footer }], + }; + }), +}; + +// ─── Close Agent Chat ───────────────────────────────────────────── + +const CloseAgentChatInput = z.object({ + chatId: z.string().describe("The chat ID to close."), +}); + +export const closeAgentChatTool = { + name: toolsMetadata.close_agent_chat.name, + title: toolsMetadata.close_agent_chat.title, + description: toolsMetadata.close_agent_chat.description, + inputSchema: CloseAgentChatInput.shape, + handler: toolHandler(CloseAgentChatInput.shape, async (input, { ctx }) => { + ctx.logger?.log("calling close_agent_chat", { input }); + + const session = activeSessions.get(input.chatId); + if (!session) { + return respondWithError( + `No active chat with ID "${input.chatId}".` + ); + } + + if (session.runId) { + try { + await session.apiClient.appendToSessionStream( + session.sessionId, + "in", + serializeInputChunk({ + kind: "message", + payload: { + messages: [], + chatId: session.chatId, + trigger: "close", + }, + }) + ); + } catch { + // Best effort — run may already be done + } + } + + activeSessions.delete(input.chatId); + + return { + content: [ + { + type: "text", + text: `Chat ${input.chatId} closed.`, + }, + ], + }; + }), +}; + +// ─── Stream collector ───────────────────────────────────────────── + +async function collectAgentResponse( + session: ChatSession +): Promise<{ text: string; toolCalls: string[]; assistantMessage: ChatMessage }> { + const baseURL = session.apiClient.baseUrl; + const streamUrl = `${baseURL}/realtime/v1/sessions/${encodeURIComponent(session.sessionId)}/out`; + + const subscription = new SSEStreamSubscription(streamUrl, { + headers: { + Authorization: `Bearer ${session.apiClient.accessToken}`, + }, + timeoutInSeconds: 120, + lastEventId: session.lastEventId, + }); + + const sseStream = await subscription.subscribe(); + const reader = sseStream.getReader(); + + let text = ""; + const toolCalls: string[] = []; + const parts: Array<{ type: string; [key: string]: unknown }> = []; + // Track current text part to accumulate deltas + let currentTextId: string | undefined; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + + if (value.id) { + session.lastEventId = value.id; + } + + // v2 (session) SSE already parses record.body.data, so `chunk` is + // the UIMessageChunk object written by the agent. + if (value.chunk != null && typeof value.chunk === "object") { + const chunk = value.chunk as Record; + + if (chunk.type === "trigger:turn-complete") { + break; + } + + if (chunk.type === "trigger:upgrade-required") { + // Agent requested upgrade — trigger continuation with full history. + // Same session, new run — reuse sessionId, swap runId. + const previousRunId = session.runId; + const result = await session.apiClient.triggerTask(session.agentId, { + payload: { + messages: session.messages, + chatId: session.chatId, + sessionId: session.sessionId, + trigger: "submit-message", + metadata: session.clientData, + continuation: true, + previousRunId, + }, + options: { + payloadType: "application/json", + tags: [`chat:${session.chatId}`], + }, + }); + session.runId = result.id; + session.lastEventId = undefined; + reader.releaseLock(); + // Recurse — subscribe to the new run's stream (same session.out URL) + return collectAgentResponse(session); + } + + if (chunk.type === "text-delta" && typeof chunk.delta === "string") { + text += chunk.delta; + // Accumulate into a text part + const textId = (chunk.id as string) ?? "text"; + if (currentTextId !== textId) { + currentTextId = textId; + parts.push({ type: "text", text: chunk.delta }); + } else { + const last = parts[parts.length - 1]; + if (last && last.type === "text") { + last.text = (last.text as string) + chunk.delta; + } + } + } + + if (chunk.type === "tool-input-available" && typeof chunk.toolName === "string") { + toolCalls.push(chunk.toolName); + parts.push({ + type: `tool-${chunk.toolName}`, + toolCallId: chunk.toolCallId as string, + toolName: chunk.toolName, + state: "input-available", + input: chunk.input, + }); + } + + if (chunk.type === "tool-output-available" && typeof chunk.toolCallId === "string") { + // Update existing tool part with output + const toolPart = parts.find( + (p) => p.toolCallId === chunk.toolCallId + ); + if (toolPart) { + toolPart.state = "output-available"; + toolPart.output = chunk.output; + } + } + } + } + } finally { + reader.releaseLock(); + } + + const assistantMessage: ChatMessage = { + id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + role: "assistant", + parts: parts.length > 0 ? parts : [{ type: "text", text }], + }; + + return { text, toolCalls, assistantMessage }; +} + +// ─── Response formatter ────────────────────────────────────────── + +function formatAssistantParts( + parts: Array<{ type: string; [key: string]: unknown }> +): string { + const sections: string[] = []; + + for (const part of parts) { + if (part.type === "text" && typeof part.text === "string" && part.text) { + sections.push(part.text); + } else if (part.type.startsWith("tool-") && part.toolName) { + const name = part.toolName as string; + const input = part.input; + const output = part.output; + + let toolSection = `[Tool: ${name}]`; + if (input != null) { + toolSection += `\nInput: ${compactJson(input)}`; + } + if (output != null) { + toolSection += `\nOutput: ${compactJson(output)}`; + } + sections.push(toolSection); + } + } + + return sections.join("\n\n"); +} + +function compactJson(value: unknown): string { + const str = JSON.stringify(value); + // Keep short values inline, truncate long ones + if (str.length <= 200) return str; + return str.slice(0, 200) + "…"; +} diff --git a/packages/cli-v3/src/mcp/tools/agents.ts b/packages/cli-v3/src/mcp/tools/agents.ts new file mode 100644 index 00000000000..e40bcafab6d --- /dev/null +++ b/packages/cli-v3/src/mcp/tools/agents.ts @@ -0,0 +1,71 @@ +import { toolsMetadata } from "../config.js"; +import { CommonProjectsInput } from "../schemas.js"; +import { respondWithError, toolHandler } from "../utils.js"; + +export const listAgentsTool = { + name: toolsMetadata.list_agents.name, + title: toolsMetadata.list_agents.title, + description: toolsMetadata.list_agents.description, + inputSchema: CommonProjectsInput.shape, + handler: toolHandler(CommonProjectsInput.shape, async (input, { ctx }) => { + ctx.logger?.log("calling list_agents", { input }); + + if (ctx.options.devOnly && input.environment !== "dev") { + return respondWithError( + `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + ); + } + + const projectRef = await ctx.getProjectRef({ + projectRef: input.projectRef, + cwd: input.configPath, + }); + + const cliApiClient = await ctx.getCliApiClient(input.branch); + + const workerResult = await cliApiClient.getWorkerByTag( + projectRef, + input.environment, + "current" + ); + + if (!workerResult.success) { + return respondWithError(workerResult.error); + } + + const { worker } = workerResult.data; + const agents = worker.tasks.filter((t) => t.triggerSource === "AGENT"); + + if (agents.length === 0) { + return { + content: [ + { + type: "text", + text: `No agents found in the current worker (${worker.version}) for ${input.environment}. Agents are tasks created with chat.agent() or chat.customAgent().`, + }, + ], + }; + } + + const contents = [ + `Found ${agents.length} agent${agents.length === 1 ? "" : "s"} in worker ${worker.version} (${input.environment}):`, + "", + ]; + + for (const agent of agents) { + contents.push(`- **${agent.slug}** (${agent.filePath})`); + } + + contents.push(""); + contents.push( + "Use `start_agent_chat` with an agent's slug as the `agentId` to start a conversation." + ); + contents.push( + "Use `get_task_schema` with an agent's slug to see its payload schema." + ); + + return { + content: [{ type: "text", text: contents.join("\n") }], + }; + }), +}; diff --git a/packages/cli-v3/src/mcp/tools/tasks.ts b/packages/cli-v3/src/mcp/tools/tasks.ts index fda3cc8943e..e82929db485 100644 --- a/packages/cli-v3/src/mcp/tools/tasks.ts +++ b/packages/cli-v3/src/mcp/tools/tasks.ts @@ -44,7 +44,8 @@ export const getCurrentWorker = { contents.push(`The worker has ${worker.tasks.length} tasks registered:`); for (const task of worker.tasks) { - contents.push(`- ${task.slug} in ${task.filePath}`); + const label = task.triggerSource === "AGENT" ? " [agent]" : ""; + contents.push(`- ${task.slug}${label} in ${task.filePath}`); } contents.push(""); diff --git a/references/ai-chat/.gitignore b/references/ai-chat/.gitignore new file mode 100644 index 00000000000..30838110ecc --- /dev/null +++ b/references/ai-chat/.gitignore @@ -0,0 +1 @@ +lib/generated/ diff --git a/references/ai-chat/.nvmrc b/references/ai-chat/.nvmrc new file mode 100644 index 00000000000..92f279e3e66 --- /dev/null +++ b/references/ai-chat/.nvmrc @@ -0,0 +1 @@ +v22 \ No newline at end of file diff --git a/references/ai-chat/DEMO-SHORTHAND.md b/references/ai-chat/DEMO-SHORTHAND.md new file mode 100644 index 00000000000..ff846401a65 --- /dev/null +++ b/references/ai-chat/DEMO-SHORTHAND.md @@ -0,0 +1,51 @@ +# Demo Cheat Sheet + +## Pitch +- Started as workflow engine, now people building chat agents +- Deep AI SDK useChat integration +- One chat = one persistent isolated execution environment +- Two-way communication + +## 1. Preloading +- Click New Chat, DON'T type anything +- Flip to dashboard — run already executing +- "waiting for first message" span +- Zero cold start + +## 2. First message — PostHog query +- "What are the top events on our PostHog instance this week?" +- Watch posthogQuery tool call +- Real data, real HogQL +- Show trace: onTurnStart → run → tool call → response +- Run stays alive after turn + +## 3. Follow-up — incremental +- "Which of those are custom events vs autocapture?" +- Only new message sent, not full history +- Backend has context in memory +- Same execution environment + +## 4. Suspend/resume +- 60s idle → snapshot → suspend → zero compute +- Next message → restore → continue +- Same run, same state + +## 5. Tool subtasks +- "Can you research what's new with PostHog lately?" +- deepResearch = separate task, own container +- Streams progress back to chat +- Show trace: triggerAndSubscribe → child run +- Stop cancels child automatically + +## 6. Code +- All regions collapsed — show the skeleton +- idleTimeoutInSeconds, clientDataSchema +- Hooks: onPreload, onTurnStart, onTurnComplete, run +- Expand run: just return streamText() +- Expand onTurnComplete: background self-review, chat.inject() + +## Wrap +- One chat, one persistent run +- Lifecycle hooks, streaming, subtasks, background injection +- Snapshot/restore, full observability +- Available now diff --git a/references/ai-chat/DEMO.md b/references/ai-chat/DEMO.md new file mode 100644 index 00000000000..5cdb560317d --- /dev/null +++ b/references/ai-chat/DEMO.md @@ -0,0 +1,96 @@ +# AI Chat Demo Script (5-7 min) + +**Setup:** Three windows ready — ai-chat app (localhost:3000), Trigger.dev dashboard, VS Code with chat.ts open (all regions collapsed). + +**Audience:** PostHog event + +**Pitch:** Trigger.dev started as a workflow engine for async background tasks, but more and more people are using us to build full chat agents. We've built a deep integration with the AI SDK's useChat hook that connects a single chat to a single persisted, isolated, fully customizable execution environment with two-way communication. + +--- + +## 1. New chat — preloading (1 min) + +**Open localhost:3000. Click "New Chat".** + +> I haven't typed anything yet. But flip to the dashboard — + +**Switch to dashboard. Show the run that just started.** + +> There's already a run executing. This is preloading. When the user opens the chat page, the frontend calls `transport.preload()` which triggers the task immediately. It loaded the user from the DB, resolved the system prompt, created the chat record — all before the first keystroke. Imagine this in something like PostHog's AI product assistant — when a user opens the chat, you want the agent ready instantly, not cold-starting while they wait. + +**Point to the "waiting for first message" span.** + +--- + +## 2. First message + live analytics query (1.5 min) + +**Switch back to chat. Type: "What are the top events on our PostHog instance this week?"** + +> Now the first turn starts — and watch, it's going to call the posthogQuery tool. This tool writes a HogQL query and runs it against our actual PostHog instance — this is our real Trigger.dev analytics data. + +**Watch the tool call + results stream back.** + +> It wrote the query, executed it, and summarized the results — all in one turn. + +**Switch to dashboard, show turn 1 span with the tool call.** + +> Here's the lifecycle — onTurnStart persisted the message, run() called streamText, the LLM decided to use the posthogQuery tool, got the results, and generated a response. After the turn completes, the run doesn't end — it waits for the next message. Same process, same memory. + +--- + +## 3. Follow-up — incremental sends + persistent state (45s) + +**Switch back to chat. Send: "How does that compare to last week?" or "Which of those are custom events vs autocapture?"** + +**Switch to dashboard, show turn 2.** + +> Turn 2 — the frontend only sent the new user message, not the full conversation. The backend already has the accumulated context. It knows what "those" refers to because it's the same execution environment. For a product analytics assistant where users iteratively drill into their data, this is huge — no context lost between turns. + +--- + +## 4. Idle, suspend, resume (30s) + +> After 60 seconds of no messages, the run snapshots its state and suspends. Zero compute while the user is away. When they come back — maybe they went to check their PostHog dashboard based on what the agent told them and came back with a follow-up — we restore from the snapshot and continue. Same run, same state. + +**Point to the "suspended" span in the trace if visible.** + +--- + +## 5. Tool subtasks (1 min) + +**Switch back to chat. Send: "Can you research what's new with PostHog lately?"** + +> Now it's using the deepResearch tool — this one is different. It's a separate Trigger.dev task running in its own container, fetching multiple URLs and streaming progress back to the chat in real time. You could have tools for querying PostHog, tools for checking feature flags, tools for pulling session recordings — and the heavy ones run as subtasks with their own retries and traces. + +**Show the trace — triggerAndSubscribe span with child run nested inside.** + +> The parent subscribes to the child via realtime. If the user hits stop, the child gets cancelled automatically. + +--- + +## 6. The code (1.5 min) + +**Switch to VS Code with chat.ts, all regions collapsed.** + +> This is the whole thing — one file. A chat.task with lifecycle hooks and a run function. + +Point out the collapsed view: + +- `idleTimeoutInSeconds`, `clientDataSchema` — typed metadata from the frontend +- `onPreload` — that's what fired before the first message +- `onTurnStart`, `onTurnComplete` — persistence hooks +- `run` — just `return streamText()`. The SDK handles everything else. + +**Expand the run region.** + +> Messages come in already converted. You return streamText. The posthogQuery tool is just a plain AI SDK tool that calls the PostHog API — deepResearch is a subtask wrapped with ai.tool. Mix and match. + +**Expand onTurnComplete if time.** + +> After every turn we defer a background call to gpt-4o-mini that reviews the response with generateObject. If it finds improvements, chat.inject adds a system message before the next LLM call. The agent gets coaching between turns — and it doesn't block the user. + +--- + +## 7. Wrap up (15s) + +> One chat, one persistent run. Lifecycle hooks, streaming, tool subtasks, background self-improvement — all on Trigger.dev's infrastructure with snapshot/restore and full observability. This is available now in the SDK. diff --git a/references/ai-chat/README.md b/references/ai-chat/README.md new file mode 100644 index 00000000000..39a6038f8c8 --- /dev/null +++ b/references/ai-chat/README.md @@ -0,0 +1,62 @@ +# AI Chat Reference App + +A multi-turn chat app built with the AI SDK's `useChat` hook and Trigger.dev's `chat.task`. Conversations run as durable Trigger.dev tasks with realtime streaming, automatic message accumulation, and persistence across page refreshes. + +## Data Models + +### Chat + +The conversation itself — your application data. + +| Column | Description | +| ---------- | ---------------------------------------- | +| `id` | Unique chat ID (generated on the client) | +| `title` | Display title for the sidebar | +| `messages` | Full `UIMessage[]` history (JSON) | + +A Chat lives forever (until the user deletes it). It is independent of any particular Trigger.dev run. + +### ChatSession + +The transport's connection state for a chat — what the frontend needs to reconnect to the same Trigger.dev run after a page refresh. + +| Column | Description | +| ------------------- | --------------------------------------------------------------------------- | +| `id` | Same as the chat ID (1:1 relationship) | +| `runId` | The Trigger.dev run handling this conversation | +| `publicAccessToken` | Scoped token for reading the run's stream and sending input stream messages | +| `lastEventId` | Stream position — used to resume without replaying old events | + +A Chat can outlive many ChatSessions. When the run ends (turn timeout, max turns reached, crash), the ChatSession is gone but the Chat and its messages remain. The next message from the user starts a fresh run and creates a new ChatSession for the same Chat. + +**Think of it as: Chat = the conversation, ChatSession = the live connection to the run handling it.** + +## Lifecycle Hooks + +Persistence is handled server-side in the Trigger.dev task via three hooks: + +- **`onChatStart`** — Creates the Chat and ChatSession records when a new conversation starts (turn 0). +- **`onTurnStart`** — Saves messages and updates the session _before_ streaming begins, so a mid-stream page refresh still shows the user's message. +- **`onTurnComplete`** — Saves the assistant's response and the `lastEventId` for stream resumption. + +## Setup + +```bash +# From the repo root +pnpm run docker # Start PostgreSQL, Redis, Electric +pnpm run db:migrate # Run webapp migrations +pnpm run db:seed # Seed the database + +# Set up the reference app's database +cd references/ai-chat +cp .env.example .env # Edit DATABASE_URL if needed +npx prisma migrate deploy + +# Build and run +pnpm run build --filter trigger.dev --filter @trigger.dev/sdk +pnpm run dev --filter webapp # In one terminal +cd references/ai-chat && pnpm exec trigger dev # In another +cd references/ai-chat && pnpm run dev # In another +``` + +Open http://localhost:3000 to use the chat app. diff --git a/references/ai-chat/lib/generated/prisma/browser.ts b/references/ai-chat/lib/generated/prisma/browser.ts new file mode 100644 index 00000000000..89081d781b0 --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/browser.ts @@ -0,0 +1,34 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file should be your main import to use Prisma-related types and utilities in a browser. + * Use it to get access to models, enums, and input types. + * + * This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only. + * See `client.ts` for the standard, server-side entry point. + * + * 🟢 You can import this file directly. + */ + +import * as Prisma from './internal/prismaNamespaceBrowser' +export { Prisma } +export * as $Enums from './enums' +export * from './enums'; +/** + * Model User + * + */ +export type User = Prisma.UserModel +/** + * Model Chat + * + */ +export type Chat = Prisma.ChatModel +/** + * Model ChatSession + * + */ +export type ChatSession = Prisma.ChatSessionModel diff --git a/references/ai-chat/lib/generated/prisma/client.ts b/references/ai-chat/lib/generated/prisma/client.ts new file mode 100644 index 00000000000..0549b038d9f --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/client.ts @@ -0,0 +1,58 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types. + * If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead. + * + * 🟢 You can import this file directly. + */ + +import * as process from 'node:process' +import * as path from 'node:path' +import { fileURLToPath } from 'node:url' +globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url)) + +import * as runtime from "@prisma/client/runtime/client" +import * as $Enums from "./enums" +import * as $Class from "./internal/class" +import * as Prisma from "./internal/prismaNamespace" + +export * as $Enums from './enums' +export * from "./enums" +/** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient({ + * adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) + * }) + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://pris.ly/d/client). + */ +export const PrismaClient = $Class.getPrismaClientClass() +export type PrismaClient = $Class.PrismaClient +export { Prisma } + +/** + * Model User + * + */ +export type User = Prisma.UserModel +/** + * Model Chat + * + */ +export type Chat = Prisma.ChatModel +/** + * Model ChatSession + * + */ +export type ChatSession = Prisma.ChatSessionModel diff --git a/references/ai-chat/lib/generated/prisma/commonInputTypes.ts b/references/ai-chat/lib/generated/prisma/commonInputTypes.ts new file mode 100644 index 00000000000..396afbb3ee3 --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/commonInputTypes.ts @@ -0,0 +1,351 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports various common sort, input & filter types that are not directly linked to a particular model. + * + * 🟢 You can import this file directly. + */ + +import type * as runtime from "@prisma/client/runtime/client" +import * as $Enums from "./enums" +import type * as Prisma from "./internal/prismaNamespace" + + +export type StringFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringFilter<$PrismaModel> | string +} + +export type StringNullableFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null +} + +export type IntFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntFilter<$PrismaModel> | number +} + +export type DateTimeFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string +} + +export type SortOrderInput = { + sort: Prisma.SortOrder + nulls?: Prisma.NullsOrder +} + +export type StringWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedStringFilter<$PrismaModel> + _max?: Prisma.NestedStringFilter<$PrismaModel> +} + +export type StringNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null + _count?: Prisma.NestedIntNullableFilter<$PrismaModel> + _min?: Prisma.NestedStringNullableFilter<$PrismaModel> + _max?: Prisma.NestedStringNullableFilter<$PrismaModel> +} + +export type IntWithAggregatesFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number + _count?: Prisma.NestedIntFilter<$PrismaModel> + _avg?: Prisma.NestedFloatFilter<$PrismaModel> + _sum?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedIntFilter<$PrismaModel> + _max?: Prisma.NestedIntFilter<$PrismaModel> +} + +export type DateTimeWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedDateTimeFilter<$PrismaModel> + _max?: Prisma.NestedDateTimeFilter<$PrismaModel> +} + +export type JsonFilter<$PrismaModel = never> = +| Prisma.PatchUndefined< + Prisma.Either>, Exclude>, 'path'>>, + Required> + > +| Prisma.OptionalFlat>, 'path'>> + +export type JsonFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter + path?: string[] + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel> + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter +} + +export type JsonWithAggregatesFilter<$PrismaModel = never> = +| Prisma.PatchUndefined< + Prisma.Either>, Exclude>, 'path'>>, + Required> + > +| Prisma.OptionalFlat>, 'path'>> + +export type JsonWithAggregatesFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter + path?: string[] + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel> + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedJsonFilter<$PrismaModel> + _max?: Prisma.NestedJsonFilter<$PrismaModel> +} + +export type NestedStringFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringFilter<$PrismaModel> | string +} + +export type NestedStringNullableFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null +} + +export type NestedIntFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntFilter<$PrismaModel> | number +} + +export type NestedDateTimeFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string +} + +export type NestedStringWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedStringFilter<$PrismaModel> + _max?: Prisma.NestedStringFilter<$PrismaModel> +} + +export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null + _count?: Prisma.NestedIntNullableFilter<$PrismaModel> + _min?: Prisma.NestedStringNullableFilter<$PrismaModel> + _max?: Prisma.NestedStringNullableFilter<$PrismaModel> +} + +export type NestedIntNullableFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null +} + +export type NestedIntWithAggregatesFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number + _count?: Prisma.NestedIntFilter<$PrismaModel> + _avg?: Prisma.NestedFloatFilter<$PrismaModel> + _sum?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedIntFilter<$PrismaModel> + _max?: Prisma.NestedIntFilter<$PrismaModel> +} + +export type NestedFloatFilter<$PrismaModel = never> = { + equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> + lt?: number | Prisma.FloatFieldRefInput<$PrismaModel> + lte?: number | Prisma.FloatFieldRefInput<$PrismaModel> + gt?: number | Prisma.FloatFieldRefInput<$PrismaModel> + gte?: number | Prisma.FloatFieldRefInput<$PrismaModel> + not?: Prisma.NestedFloatFilter<$PrismaModel> | number +} + +export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedDateTimeFilter<$PrismaModel> + _max?: Prisma.NestedDateTimeFilter<$PrismaModel> +} + +export type NestedJsonFilter<$PrismaModel = never> = +| Prisma.PatchUndefined< + Prisma.Either>, Exclude>, 'path'>>, + Required> + > +| Prisma.OptionalFlat>, 'path'>> + +export type NestedJsonFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter + path?: string[] + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel> + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter +} + + diff --git a/references/ai-chat/lib/generated/prisma/enums.ts b/references/ai-chat/lib/generated/prisma/enums.ts new file mode 100644 index 00000000000..043572d9f3f --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/enums.ts @@ -0,0 +1,15 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* +* This file exports all enum related types from the schema. +* +* 🟢 You can import this file directly. +*/ + + + +// This file is empty because there are no enums in the schema. +export {} diff --git a/references/ai-chat/lib/generated/prisma/internal/class.ts b/references/ai-chat/lib/generated/prisma/internal/class.ts new file mode 100644 index 00000000000..998f62430c4 --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/internal/class.ts @@ -0,0 +1,224 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * Please import the `PrismaClient` class from the `client.ts` file instead. + */ + +import * as runtime from "@prisma/client/runtime/client" +import type * as Prisma from "./prismaNamespace" + + +const config: runtime.GetPrismaClientConfig = { + "previewFeatures": [], + "clientVersion": "7.4.2", + "engineVersion": "94a226be1cf2967af2541cca5529f0f7ba866919", + "activeProvider": "postgresql", + "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"../lib/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id String @id\n name String\n plan String @default(\"free\") // \"free\" | \"pro\"\n preferredModel String?\n githubToken String?\n messageCount Int @default(0)\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n chats Chat[]\n}\n\nmodel Chat {\n id String @id\n title String\n model String @default(\"gpt-4o-mini\")\n messages Json @default(\"[]\")\n userId String?\n user User? @relation(fields: [userId], references: [id])\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\n// Persistable session state for a chat. After the Sessions-as-run-manager\n// refactor, the transport addresses by `chatId` (used as the Session\n// `externalId`) on every wire path — so we only need a session-scoped\n// PAT and the SSE last-event-id for resume. Runs come and go inside\n// the Session and are managed server-side.\nmodel ChatSession {\n id String @id // chatId\n publicAccessToken String\n lastEventId String?\n}\n", + "runtimeDataModel": { + "models": {}, + "enums": {}, + "types": {} + }, + "parameterizationSchema": { + "strings": [], + "graph": "" + } +} + +config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"plan\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"preferredModel\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"githubToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"messageCount\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"chats\",\"kind\":\"object\",\"type\":\"Chat\",\"relationName\":\"ChatToUser\"}],\"dbName\":null},\"Chat\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"model\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"messages\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ChatToUser\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ChatSession\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"publicAccessToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastEventId\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") +config.parameterizationSchema = { + strings: JSON.parse("[\"where\",\"orderBy\",\"cursor\",\"user\",\"chats\",\"_count\",\"User.findUnique\",\"User.findUniqueOrThrow\",\"User.findFirst\",\"User.findFirstOrThrow\",\"User.findMany\",\"data\",\"User.createOne\",\"User.createMany\",\"User.createManyAndReturn\",\"User.updateOne\",\"User.updateMany\",\"User.updateManyAndReturn\",\"create\",\"update\",\"User.upsertOne\",\"User.deleteOne\",\"User.deleteMany\",\"having\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"User.groupBy\",\"User.aggregate\",\"Chat.findUnique\",\"Chat.findUniqueOrThrow\",\"Chat.findFirst\",\"Chat.findFirstOrThrow\",\"Chat.findMany\",\"Chat.createOne\",\"Chat.createMany\",\"Chat.createManyAndReturn\",\"Chat.updateOne\",\"Chat.updateMany\",\"Chat.updateManyAndReturn\",\"Chat.upsertOne\",\"Chat.deleteOne\",\"Chat.deleteMany\",\"Chat.groupBy\",\"Chat.aggregate\",\"ChatSession.findUnique\",\"ChatSession.findUniqueOrThrow\",\"ChatSession.findFirst\",\"ChatSession.findFirstOrThrow\",\"ChatSession.findMany\",\"ChatSession.createOne\",\"ChatSession.createMany\",\"ChatSession.createManyAndReturn\",\"ChatSession.updateOne\",\"ChatSession.updateMany\",\"ChatSession.updateManyAndReturn\",\"ChatSession.upsertOne\",\"ChatSession.deleteOne\",\"ChatSession.deleteMany\",\"ChatSession.groupBy\",\"ChatSession.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"publicAccessToken\",\"lastEventId\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"contains\",\"startsWith\",\"endsWith\",\"not\",\"title\",\"model\",\"messages\",\"userId\",\"createdAt\",\"updatedAt\",\"string_contains\",\"string_starts_with\",\"string_ends_with\",\"array_starts_with\",\"array_ends_with\",\"array_contains\",\"name\",\"plan\",\"preferredModel\",\"githubToken\",\"messageCount\",\"every\",\"some\",\"none\",\"is\",\"isNot\",\"connectOrCreate\",\"upsert\",\"createMany\",\"set\",\"disconnect\",\"delete\",\"connect\",\"updateMany\",\"deleteMany\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), + graph: "lQEcMAwEAABqACA-AABnADA_AAAHABBAAABnADBBAQAAAAFTQABpACFUQABpACFbAQBbACFcAQBbACFdAQBcACFeAQBcACFfAgBoACEBAAAAAQAgCwMAAG0AID4AAGsAMD8AAAMAEEAAAGsAMEEBAFsAIU8BAFsAIVABAFsAIVEAAGwAIFIBAFwAIVNAAGkAIVRAAGkAIQIDAACPAQAgUgAAbgAgCwMAAG0AID4AAGsAMD8AAAMAEEAAAGsAMEEBAAAAAU8BAFsAIVABAFsAIVEAAGwAIFIBAFwAIVNAAGkAIVRAAGkAIQMAAAADACABAAAEADACAAAFACAMBAAAagAgPgAAZwAwPwAABwAQQAAAZwAwQQEAWwAhU0AAaQAhVEAAaQAhWwEAWwAhXAEAWwAhXQEAXAAhXgEAXAAhXwIAaAAhAQAAAAcAIAEAAAADACABAAAAAQAgAwQAAI4BACBdAABuACBeAABuACADAAAABwAgAQAACwAwAgAAAQAgAwAAAAcAIAEAAAsAMAIAAAEAIAMAAAAHACABAAALADACAAABACAJBAAAjQEAIEEBAAAAAVNAAAAAAVRAAAAAAVsBAAAAAVwBAAAAAV0BAAAAAV4BAAAAAV8CAAAAAQELAAAPACAIQQEAAAABU0AAAAABVEAAAAABWwEAAAABXAEAAAABXQEAAAABXgEAAAABXwIAAAABAQsAABEAMAELAAARADAJBAAAgAEAIEEBAHIAIVNAAHcAIVRAAHcAIVsBAHIAIVwBAHIAIV0BAHMAIV4BAHMAIV8CAH8AIQIAAAABACALAAAUACAIQQEAcgAhU0AAdwAhVEAAdwAhWwEAcgAhXAEAcgAhXQEAcwAhXgEAcwAhXwIAfwAhAgAAAAcAIAsAABYAIAIAAAAHACALAAAWACADAAAAAQAgEgAADwAgEwAAFAAgAQAAAAEAIAEAAAAHACAHBQAAegAgGAAAewAgGQAAfgAgGgAAfQAgGwAAfAAgXQAAbgAgXgAAbgAgCz4AAGMAMD8AAB0AEEAAAGMAMEEBAFIAIVNAAF8AIVRAAF8AIVsBAFIAIVwBAFIAIV0BAFMAIV4BAFMAIV8CAGQAIQMAAAAHACABAAAcADAXAAAdACADAAAABwAgAQAACwAwAgAAAQAgAQAAAAUAIAEAAAAFACADAAAAAwAgAQAABAAwAgAABQAgAwAAAAMAIAEAAAQAMAIAAAUAIAMAAAADACABAAAEADACAAAFACAIAwAAeQAgQQEAAAABTwEAAAABUAEAAAABUYAAAAABUgEAAAABU0AAAAABVEAAAAABAQsAACUAIAdBAQAAAAFPAQAAAAFQAQAAAAFRgAAAAAFSAQAAAAFTQAAAAAFUQAAAAAEBCwAAJwAwAQsAACcAMAEAAAAHACAIAwAAeAAgQQEAcgAhTwEAcgAhUAEAcgAhUYAAAAABUgEAcwAhU0AAdwAhVEAAdwAhAgAAAAUAIAsAACsAIAdBAQByACFPAQByACFQAQByACFRgAAAAAFSAQBzACFTQAB3ACFUQAB3ACECAAAAAwAgCwAALQAgAgAAAAMAIAsAAC0AIAEAAAAHACADAAAABQAgEgAAJQAgEwAAKwAgAQAAAAUAIAEAAAADACAEBQAAdAAgGgAAdgAgGwAAdQAgUgAAbgAgCj4AAF0AMD8AADUAEEAAAF0AMEEBAFIAIU8BAFIAIVABAFIAIVEAAF4AIFIBAFMAIVNAAF8AIVRAAF8AIQMAAAADACABAAA0ADAXAAA1ACADAAAAAwAgAQAABAAwAgAABQAgBj4AAFoAMD8AADsAEEAAAFoAMEEBAAAAAUIBAFsAIUMBAFwAIQEAAAA4ACABAAAAOAAgBj4AAFoAMD8AADsAEEAAAFoAMEEBAFsAIUIBAFsAIUMBAFwAIQFDAABuACADAAAAOwAgAQAAPAAwAgAAOAAgAwAAADsAIAEAADwAMAIAADgAIAMAAAA7ACABAAA8ADACAAA4ACADQQEAAAABQgEAAAABQwEAAAABAQsAAEAAIANBAQAAAAFCAQAAAAFDAQAAAAEBCwAAQgAwAQsAAEIAMANBAQByACFCAQByACFDAQBzACECAAAAOAAgCwAARQAgA0EBAHIAIUIBAHIAIUMBAHMAIQIAAAA7ACALAABHACACAAAAOwAgCwAARwAgAwAAADgAIBIAAEAAIBMAAEUAIAEAAAA4ACABAAAAOwAgBAUAAG8AIBoAAHEAIBsAAHAAIEMAAG4AIAY-AABRADA_AABOABBAAABRADBBAQBSACFCAQBSACFDAQBTACEDAAAAOwAgAQAATQAwFwAATgAgAwAAADsAIAEAADwAMAIAADgAIAY-AABRADA_AABOABBAAABRADBBAQBSACFCAQBSACFDAQBTACEOBQAAWAAgGgAAWQAgGwAAWQAgRAEAAAABRQEAAAAERgEAAAAERwEAAAABSAEAAAABSQEAAAABSgEAAAABSwEAAAABTAEAAAABTQEAAAABTgEAVwAhDgUAAFUAIBoAAFYAIBsAAFYAIEQBAAAAAUUBAAAABUYBAAAABUcBAAAAAUgBAAAAAUkBAAAAAUoBAAAAAUsBAAAAAUwBAAAAAU0BAAAAAU4BAFQAIQ4FAABVACAaAABWACAbAABWACBEAQAAAAFFAQAAAAVGAQAAAAVHAQAAAAFIAQAAAAFJAQAAAAFKAQAAAAFLAQAAAAFMAQAAAAFNAQAAAAFOAQBUACEIRAIAAAABRQIAAAAFRgIAAAAFRwIAAAABSAIAAAABSQIAAAABSgIAAAABTgIAVQAhC0QBAAAAAUUBAAAABUYBAAAABUcBAAAAAUgBAAAAAUkBAAAAAUoBAAAAAUsBAAAAAUwBAAAAAU0BAAAAAU4BAFYAIQ4FAABYACAaAABZACAbAABZACBEAQAAAAFFAQAAAARGAQAAAARHAQAAAAFIAQAAAAFJAQAAAAFKAQAAAAFLAQAAAAFMAQAAAAFNAQAAAAFOAQBXACEIRAIAAAABRQIAAAAERgIAAAAERwIAAAABSAIAAAABSQIAAAABSgIAAAABTgIAWAAhC0QBAAAAAUUBAAAABEYBAAAABEcBAAAAAUgBAAAAAUkBAAAAAUoBAAAAAUsBAAAAAUwBAAAAAU0BAAAAAU4BAFkAIQY-AABaADA_AAA7ABBAAABaADBBAQBbACFCAQBbACFDAQBcACELRAEAAAABRQEAAAAERgEAAAAERwEAAAABSAEAAAABSQEAAAABSgEAAAABSwEAAAABTAEAAAABTQEAAAABTgEAWQAhC0QBAAAAAUUBAAAABUYBAAAABUcBAAAAAUgBAAAAAUkBAAAAAUoBAAAAAUsBAAAAAUwBAAAAAU0BAAAAAU4BAFYAIQo-AABdADA_AAA1ABBAAABdADBBAQBSACFPAQBSACFQAQBSACFRAABeACBSAQBTACFTQABfACFUQABfACEPBQAAWAAgGgAAYgAgGwAAYgAgRIAAAAABR4AAAAABSIAAAAABSYAAAAABSoAAAAABToAAAAABVQEAAAABVgEAAAABVwEAAAABWIAAAAABWYAAAAABWoAAAAABCwUAAFgAIBoAAGEAIBsAAGEAIERAAAAAAUVAAAAABEZAAAAABEdAAAAAAUhAAAAAAUlAAAAAAUpAAAAAAU5AAGAAIQsFAABYACAaAABhACAbAABhACBEQAAAAAFFQAAAAARGQAAAAARHQAAAAAFIQAAAAAFJQAAAAAFKQAAAAAFOQABgACEIREAAAAABRUAAAAAERkAAAAAER0AAAAABSEAAAAABSUAAAAABSkAAAAABTkAAYQAhDESAAAAAAUeAAAAAAUiAAAAAAUmAAAAAAUqAAAAAAU6AAAAAAVUBAAAAAVYBAAAAAVcBAAAAAViAAAAAAVmAAAAAAVqAAAAAAQs-AABjADA_AAAdABBAAABjADBBAQBSACFTQABfACFUQABfACFbAQBSACFcAQBSACFdAQBTACFeAQBTACFfAgBkACENBQAAWAAgGAAAZgAgGQAAWAAgGgAAWAAgGwAAWAAgRAIAAAABRQIAAAAERgIAAAAERwIAAAABSAIAAAABSQIAAAABSgIAAAABTgIAZQAhDQUAAFgAIBgAAGYAIBkAAFgAIBoAAFgAIBsAAFgAIEQCAAAAAUUCAAAABEYCAAAABEcCAAAAAUgCAAAAAUkCAAAAAUoCAAAAAU4CAGUAIQhECAAAAAFFCAAAAARGCAAAAARHCAAAAAFICAAAAAFJCAAAAAFKCAAAAAFOCABmACEMBAAAagAgPgAAZwAwPwAABwAQQAAAZwAwQQEAWwAhU0AAaQAhVEAAaQAhWwEAWwAhXAEAWwAhXQEAXAAhXgEAXAAhXwIAaAAhCEQCAAAAAUUCAAAABEYCAAAABEcCAAAAAUgCAAAAAUkCAAAAAUoCAAAAAU4CAFgAIQhEQAAAAAFFQAAAAARGQAAAAARHQAAAAAFIQAAAAAFJQAAAAAFKQAAAAAFOQABhACEDYAAAAwAgYQAAAwAgYgAAAwAgCwMAAG0AID4AAGsAMD8AAAMAEEAAAGsAMEEBAFsAIU8BAFsAIVABAFsAIVEAAGwAIFIBAFwAIVNAAGkAIVRAAGkAIQxEgAAAAAFHgAAAAAFIgAAAAAFJgAAAAAFKgAAAAAFOgAAAAAFVAQAAAAFWAQAAAAFXAQAAAAFYgAAAAAFZgAAAAAFagAAAAAEOBAAAagAgPgAAZwAwPwAABwAQQAAAZwAwQQEAWwAhU0AAaQAhVEAAaQAhWwEAWwAhXAEAWwAhXQEAXAAhXgEAXAAhXwIAaAAhYwAABwAgZAAABwAgAAAAAAFoAQAAAAEBaAEAAAABAAAAAWhAAAAAAQcSAACRAQAgEwAAlAEAIGUAAJIBACBmAACTAQAgaQAABwAgagAABwAgawAAAQAgAxIAAJEBACBlAACSAQAgawAAAQAgAAAAAAAFaAIAAAABbgIAAAABbwIAAAABcAIAAAABcQIAAAABCxIAAIEBADATAACGAQAwZQAAggEAMGYAAIMBADBnAACEAQAgaAAAhQEAMGkAAIUBADBqAACFAQAwawAAhQEAMGwAAIcBADBtAACIAQAwBkEBAAAAAU8BAAAAAVABAAAAAVGAAAAAAVNAAAAAAVRAAAAAAQIAAAAFACASAACMAQAgAwAAAAUAIBIAAIwBACATAACLAQAgAQsAAJABADALAwAAbQAgPgAAawAwPwAAAwAQQAAAawAwQQEAAAABTwEAWwAhUAEAWwAhUQAAbAAgUgEAXAAhU0AAaQAhVEAAaQAhAgAAAAUAIAsAAIsBACACAAAAiQEAIAsAAIoBACAKPgAAiAEAMD8AAIkBABBAAACIAQAwQQEAWwAhTwEAWwAhUAEAWwAhUQAAbAAgUgEAXAAhU0AAaQAhVEAAaQAhCj4AAIgBADA_AACJAQAQQAAAiAEAMEEBAFsAIU8BAFsAIVABAFsAIVEAAGwAIFIBAFwAIVNAAGkAIVRAAGkAIQZBAQByACFPAQByACFQAQByACFRgAAAAAFTQAB3ACFUQAB3ACEGQQEAcgAhTwEAcgAhUAEAcgAhUYAAAAABU0AAdwAhVEAAdwAhBkEBAAAAAU8BAAAAAVABAAAAAVGAAAAAAVNAAAAAAVRAAAAAAQQSAACBAQAwZQAAggEAMGcAAIQBACBrAACFAQAwAAMEAACOAQAgXQAAbgAgXgAAbgAgBkEBAAAAAU8BAAAAAVABAAAAAVGAAAAAAVNAAAAAAVRAAAAAAQhBAQAAAAFTQAAAAAFUQAAAAAFbAQAAAAFcAQAAAAFdAQAAAAFeAQAAAAFfAgAAAAECAAAAAQAgEgAAkQEAIAMAAAAHACASAACRAQAgEwAAlQEAIAoAAAAHACALAACVAQAgQQEAcgAhU0AAdwAhVEAAdwAhWwEAcgAhXAEAcgAhXQEAcwAhXgEAcwAhXwIAfwAhCEEBAHIAIVNAAHcAIVRAAHcAIVsBAHIAIVwBAHIAIV0BAHMAIV4BAHMAIV8CAH8AIQIEBgIFAAMBAwgBAQQJAAAAAAUFAAgYAAkZAAoaAAsbAAwAAAAAAAUFAAgYAAkZAAoaAAsbAAwBAyoBAQMwAQMFABEaABIbABMAAAADBQARGgASGwATAAAAAwUAGRoAGhsAGwAAAAMFABkaABobABsGAgEHCgEIDAEJDQEKDgEMEAENEgQOEwUPFQEQFwQRGAYUGQEVGgEWGwQcHgcdHw0eIAIfIQIgIgIhIwIiJAIjJgIkKAQlKQ4mLAInLgQoLw8pMQIqMgIrMwQsNhAtNxQuORUvOhUwPRUxPhUyPxUzQRU0QwQ1RBY2RhU3SAQ4SRc5ShU6SxU7TAQ8Txg9UBw" +} + +async function decodeBase64AsWasm(wasmBase64: string): Promise { + const { Buffer } = await import('node:buffer') + const wasmArray = Buffer.from(wasmBase64, 'base64') + return new WebAssembly.Module(wasmArray) +} + +config.compilerWasm = { + getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.mjs"), + + getQueryCompilerWasmModule: async () => { + const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.mjs") + return await decodeBase64AsWasm(wasm) + }, + + importName: "./query_compiler_fast_bg.js" +} + + + +export type LogOptions = + 'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array ? Prisma.GetEvents : never : never + +export interface PrismaClientConstructor { + /** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient({ + * adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) + * }) + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://pris.ly/d/client). + */ + + new < + Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions, + LogOpts extends LogOptions = LogOptions, + OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'], + ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs + >(options: Prisma.Subset ): PrismaClient +} + +/** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient({ + * adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) + * }) + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://pris.ly/d/client). + */ + +export interface PrismaClient< + in LogOpts extends Prisma.LogLevel = never, + in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined, + in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs +> { + [K: symbol]: { types: Prisma.TypeMap['other'] } + + $on(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient; + + /** + * Connect with the database + */ + $connect(): runtime.Types.Utils.JsPromise; + + /** + * Disconnect from the database + */ + $disconnect(): runtime.Types.Utils.JsPromise; + +/** + * Executes a prepared raw query and returns the number of affected rows. + * @example + * ``` + * const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};` + * ``` + * + * Read more in our [docs](https://pris.ly/d/raw-queries). + */ + $executeRaw(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise; + + /** + * Executes a raw query and returns the number of affected rows. + * Susceptible to SQL injections, see documentation. + * @example + * ``` + * const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com') + * ``` + * + * Read more in our [docs](https://pris.ly/d/raw-queries). + */ + $executeRawUnsafe(query: string, ...values: any[]): Prisma.PrismaPromise; + + /** + * Performs a prepared raw query and returns the `SELECT` data. + * @example + * ``` + * const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};` + * ``` + * + * Read more in our [docs](https://pris.ly/d/raw-queries). + */ + $queryRaw(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise; + + /** + * Performs a raw query and returns the `SELECT` data. + * Susceptible to SQL injections, see documentation. + * @example + * ``` + * const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com') + * ``` + * + * Read more in our [docs](https://pris.ly/d/raw-queries). + */ + $queryRawUnsafe(query: string, ...values: any[]): Prisma.PrismaPromise; + + + /** + * Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole. + * @example + * ``` + * const [george, bob, alice] = await prisma.$transaction([ + * prisma.user.create({ data: { name: 'George' } }), + * prisma.user.create({ data: { name: 'Bob' } }), + * prisma.user.create({ data: { name: 'Alice' } }), + * ]) + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/orm/prisma-client/queries/transactions). + */ + $transaction

[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise> + + $transaction(fn: (prisma: Omit) => runtime.Types.Utils.JsPromise, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise + + $extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb, ExtArgs, runtime.Types.Utils.Call, { + extArgs: ExtArgs + }>> + + /** + * `prisma.user`: Exposes CRUD operations for the **User** model. + * Example usage: + * ```ts + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + */ + get user(): Prisma.UserDelegate; + + /** + * `prisma.chat`: Exposes CRUD operations for the **Chat** model. + * Example usage: + * ```ts + * // Fetch zero or more Chats + * const chats = await prisma.chat.findMany() + * ``` + */ + get chat(): Prisma.ChatDelegate; + + /** + * `prisma.chatSession`: Exposes CRUD operations for the **ChatSession** model. + * Example usage: + * ```ts + * // Fetch zero or more ChatSessions + * const chatSessions = await prisma.chatSession.findMany() + * ``` + */ + get chatSession(): Prisma.ChatSessionDelegate; +} + +export function getPrismaClientClass(): PrismaClientConstructor { + return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor +} diff --git a/references/ai-chat/lib/generated/prisma/internal/prismaNamespace.ts b/references/ai-chat/lib/generated/prisma/internal/prismaNamespace.ts new file mode 100644 index 00000000000..642de7400e5 --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/internal/prismaNamespace.ts @@ -0,0 +1,981 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file. + * While this enables partial backward compatibility, it is not part of the stable public API. + * + * If you are looking for your Models, Enums, and Input Types, please import them from the respective + * model files in the `model` directory! + */ + +import * as runtime from "@prisma/client/runtime/client" +import type * as Prisma from "../models" +import { type PrismaClient } from "./class" + +export type * from '../models' + +export type DMMF = typeof runtime.DMMF + +export type PrismaPromise = runtime.Types.Public.PrismaPromise + +/** + * Prisma Errors + */ + +export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError +export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError + +export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError +export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError + +export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError +export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError + +export const PrismaClientInitializationError = runtime.PrismaClientInitializationError +export type PrismaClientInitializationError = runtime.PrismaClientInitializationError + +export const PrismaClientValidationError = runtime.PrismaClientValidationError +export type PrismaClientValidationError = runtime.PrismaClientValidationError + +/** + * Re-export of sql-template-tag + */ +export const sql = runtime.sqltag +export const empty = runtime.empty +export const join = runtime.join +export const raw = runtime.raw +export const Sql = runtime.Sql +export type Sql = runtime.Sql + + + +/** + * Decimal.js + */ +export const Decimal = runtime.Decimal +export type Decimal = runtime.Decimal + +export type DecimalJsLike = runtime.DecimalJsLike + +/** +* Extensions +*/ +export type Extension = runtime.Types.Extensions.UserArgs +export const getExtensionContext = runtime.Extensions.getExtensionContext +export type Args = runtime.Types.Public.Args +export type Payload = runtime.Types.Public.Payload +export type Result = runtime.Types.Public.Result +export type Exact = runtime.Types.Public.Exact + +export type PrismaVersion = { + client: string + engine: string +} + +/** + * Prisma Client JS version: 7.4.2 + * Query Engine version: 94a226be1cf2967af2541cca5529f0f7ba866919 + */ +export const prismaVersion: PrismaVersion = { + client: "7.4.2", + engine: "94a226be1cf2967af2541cca5529f0f7ba866919" +} + +/** + * Utility Types + */ + +export type Bytes = runtime.Bytes +export type JsonObject = runtime.JsonObject +export type JsonArray = runtime.JsonArray +export type JsonValue = runtime.JsonValue +export type InputJsonObject = runtime.InputJsonObject +export type InputJsonArray = runtime.InputJsonArray +export type InputJsonValue = runtime.InputJsonValue + + +export const NullTypes = { + DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull), + JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull), + AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull), +} +/** + * Helper for filtering JSON entries that have `null` on the database (empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const DbNull = runtime.DbNull + +/** + * Helper for filtering JSON entries that have JSON `null` values (not empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const JsonNull = runtime.JsonNull + +/** + * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull` + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const AnyNull = runtime.AnyNull + + +type SelectAndInclude = { + select: any + include: any +} + +type SelectAndOmit = { + select: any + omit: any +} + +/** + * From T, pick a set of properties whose keys are in the union K + */ +type Prisma__Pick = { + [P in K]: T[P]; +}; + +export type Enumerable = T | Array; + +/** + * Subset + * @desc From `T` pick properties that exist in `U`. Simple version of Intersection + */ +export type Subset = { + [key in keyof T]: key extends keyof U ? T[key] : never; +}; + +/** + * SelectSubset + * @desc From `T` pick properties that exist in `U`. Simple version of Intersection. + * Additionally, it validates, if both select and include are present. If the case, it errors. + */ +export type SelectSubset = { + [key in keyof T]: key extends keyof U ? T[key] : never +} & + (T extends SelectAndInclude + ? 'Please either choose `select` or `include`.' + : T extends SelectAndOmit + ? 'Please either choose `select` or `omit`.' + : {}) + +/** + * Subset + Intersection + * @desc From `T` pick properties that exist in `U` and intersect `K` + */ +export type SubsetIntersection = { + [key in keyof T]: key extends keyof U ? T[key] : never +} & + K + +type Without = { [P in Exclude]?: never }; + +/** + * XOR is needed to have a real mutually exclusive union type + * https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types + */ +export type XOR = + T extends object ? + U extends object ? + (Without & U) | (Without & T) + : U : T + + +/** + * Is T a Record? + */ +type IsObject = T extends Array +? False +: T extends Date +? False +: T extends Uint8Array +? False +: T extends BigInt +? False +: T extends object +? True +: False + + +/** + * If it's T[], return T + */ +export type UnEnumerate = T extends Array ? U : T + +/** + * From ts-toolbelt + */ + +type __Either = Omit & + { + // Merge all but K + [P in K]: Prisma__Pick // With K possibilities + }[K] + +type EitherStrict = Strict<__Either> + +type EitherLoose = ComputeRaw<__Either> + +type _Either< + O extends object, + K extends Key, + strict extends Boolean +> = { + 1: EitherStrict + 0: EitherLoose +}[strict] + +export type Either< + O extends object, + K extends Key, + strict extends Boolean = 1 +> = O extends unknown ? _Either : never + +export type Union = any + +export type PatchUndefined = { + [K in keyof O]: O[K] extends undefined ? At : O[K] +} & {} + +/** Helper Types for "Merge" **/ +export type IntersectOf = ( + U extends unknown ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never + +export type Overwrite = { + [K in keyof O]: K extends keyof O1 ? O1[K] : O[K]; +} & {}; + +type _Merge = IntersectOf; +}>>; + +type Key = string | number | symbol; +type AtStrict = O[K & keyof O]; +type AtLoose = O extends unknown ? AtStrict : never; +export type At = { + 1: AtStrict; + 0: AtLoose; +}[strict]; + +export type ComputeRaw = A extends Function ? A : { + [K in keyof A]: A[K]; +} & {}; + +export type OptionalFlat = { + [K in keyof O]?: O[K]; +} & {}; + +type _Record = { + [P in K]: T; +}; + +// cause typescript not to expand types and preserve names +type NoExpand = T extends unknown ? T : never; + +// this type assumes the passed object is entirely optional +export type AtLeast = NoExpand< + O extends unknown + ? | (K extends keyof O ? { [P in K]: O[P] } & O : O) + | {[P in keyof O as P extends K ? P : never]-?: O[P]} & O + : never>; + +type _Strict = U extends unknown ? U & OptionalFlat<_Record, keyof U>, never>> : never; + +export type Strict = ComputeRaw<_Strict>; +/** End Helper Types for "Merge" **/ + +export type Merge = ComputeRaw<_Merge>>; + +export type Boolean = True | False + +export type True = 1 + +export type False = 0 + +export type Not = { + 0: 1 + 1: 0 +}[B] + +export type Extends = [A1] extends [never] + ? 0 // anything `never` is false + : A1 extends A2 + ? 1 + : 0 + +export type Has = Not< + Extends, U1> +> + +export type Or = { + 0: { + 0: 0 + 1: 1 + } + 1: { + 0: 1 + 1: 1 + } +}[B1][B2] + +export type Keys = U extends unknown ? keyof U : never + +export type GetScalarType = O extends object ? { + [P in keyof T]: P extends keyof O + ? O[P] + : never +} : never + +type FieldPaths< + T, + U = Omit +> = IsObject extends True ? U : T + +export type GetHavingFields = { + [K in keyof T]: Or< + Or, Extends<'AND', K>>, + Extends<'NOT', K> + > extends True + ? // infer is only needed to not hit TS limit + // based on the brilliant idea of Pierre-Antoine Mills + // https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437 + T[K] extends infer TK + ? GetHavingFields extends object ? Merge> : never> + : never + : {} extends FieldPaths + ? never + : K +}[keyof T] + +/** + * Convert tuple to union + */ +type _TupleToUnion = T extends (infer E)[] ? E : never +type TupleToUnion = _TupleToUnion +export type MaybeTupleToUnion = T extends any[] ? TupleToUnion : T + +/** + * Like `Pick`, but additionally can also accept an array of keys + */ +export type PickEnumerable | keyof T> = Prisma__Pick> + +/** + * Exclude all keys with underscores + */ +export type ExcludeUnderscoreKeys = T extends `_${string}` ? never : T + + +export type FieldRef = runtime.FieldRef + +type FieldRefInputType = Model extends never ? never : FieldRef + + +export const ModelName = { + User: 'User', + Chat: 'Chat', + ChatSession: 'ChatSession' +} as const + +export type ModelName = (typeof ModelName)[keyof typeof ModelName] + + + +export interface TypeMapCb extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record> { + returns: TypeMap +} + +export type TypeMap = { + globalOmitOptions: { + omit: GlobalOmitOptions + } + meta: { + modelProps: "user" | "chat" | "chatSession" + txIsolationLevel: TransactionIsolationLevel + } + model: { + User: { + payload: Prisma.$UserPayload + fields: Prisma.UserFieldRefs + operations: { + findUnique: { + args: Prisma.UserFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.UserFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.UserFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.UserFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.UserFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.UserCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.UserCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.UserCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.UserDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.UserUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.UserDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.UserUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.UserUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.UserUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.UserAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.UserGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.UserCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + Chat: { + payload: Prisma.$ChatPayload + fields: Prisma.ChatFieldRefs + operations: { + findUnique: { + args: Prisma.ChatFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.ChatFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.ChatFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.ChatFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.ChatFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.ChatCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.ChatCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.ChatCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.ChatDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.ChatUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.ChatDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.ChatUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.ChatUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.ChatUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.ChatAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.ChatGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.ChatCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + ChatSession: { + payload: Prisma.$ChatSessionPayload + fields: Prisma.ChatSessionFieldRefs + operations: { + findUnique: { + args: Prisma.ChatSessionFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.ChatSessionFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.ChatSessionFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.ChatSessionFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.ChatSessionFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.ChatSessionCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.ChatSessionCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.ChatSessionCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.ChatSessionDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.ChatSessionUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.ChatSessionDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.ChatSessionUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.ChatSessionUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.ChatSessionUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.ChatSessionAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.ChatSessionGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.ChatSessionCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + } +} & { + other: { + payload: any + operations: { + $executeRaw: { + args: [query: TemplateStringsArray | Sql, ...values: any[]], + result: any + } + $executeRawUnsafe: { + args: [query: string, ...values: any[]], + result: any + } + $queryRaw: { + args: [query: TemplateStringsArray | Sql, ...values: any[]], + result: any + } + $queryRawUnsafe: { + args: [query: string, ...values: any[]], + result: any + } + } + } +} + +/** + * Enums + */ + +export const TransactionIsolationLevel = runtime.makeStrictEnum({ + ReadUncommitted: 'ReadUncommitted', + ReadCommitted: 'ReadCommitted', + RepeatableRead: 'RepeatableRead', + Serializable: 'Serializable' +} as const) + +export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] + + +export const UserScalarFieldEnum = { + id: 'id', + name: 'name', + plan: 'plan', + preferredModel: 'preferredModel', + githubToken: 'githubToken', + messageCount: 'messageCount', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum] + + +export const ChatScalarFieldEnum = { + id: 'id', + title: 'title', + model: 'model', + messages: 'messages', + userId: 'userId', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type ChatScalarFieldEnum = (typeof ChatScalarFieldEnum)[keyof typeof ChatScalarFieldEnum] + + +export const ChatSessionScalarFieldEnum = { + id: 'id', + publicAccessToken: 'publicAccessToken', + lastEventId: 'lastEventId' +} as const + +export type ChatSessionScalarFieldEnum = (typeof ChatSessionScalarFieldEnum)[keyof typeof ChatSessionScalarFieldEnum] + + +export const SortOrder = { + asc: 'asc', + desc: 'desc' +} as const + +export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder] + + +export const JsonNullValueInput = { + JsonNull: JsonNull +} as const + +export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput] + + +export const QueryMode = { + default: 'default', + insensitive: 'insensitive' +} as const + +export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] + + +export const NullsOrder = { + first: 'first', + last: 'last' +} as const + +export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder] + + +export const JsonNullValueFilter = { + DbNull: DbNull, + JsonNull: JsonNull, + AnyNull: AnyNull +} as const + +export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter] + + + +/** + * Field references + */ + + +/** + * Reference to a field of type 'String' + */ +export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'> + + + +/** + * Reference to a field of type 'String[]' + */ +export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String[]'> + + + +/** + * Reference to a field of type 'Int' + */ +export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'> + + + +/** + * Reference to a field of type 'Int[]' + */ +export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int[]'> + + + +/** + * Reference to a field of type 'DateTime' + */ +export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'> + + + +/** + * Reference to a field of type 'DateTime[]' + */ +export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime[]'> + + + +/** + * Reference to a field of type 'Json' + */ +export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'> + + + +/** + * Reference to a field of type 'QueryMode' + */ +export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'> + + + +/** + * Reference to a field of type 'Float' + */ +export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'> + + + +/** + * Reference to a field of type 'Float[]' + */ +export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'> + + +/** + * Batch Payload for updateMany & deleteMany & createMany + */ +export type BatchPayload = { + count: number +} + +export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs> +export type DefaultPrismaClient = PrismaClient +export type ErrorFormat = 'pretty' | 'colorless' | 'minimal' +export type PrismaClientOptions = ({ + /** + * Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`. + */ + adapter: runtime.SqlDriverAdapterFactory + accelerateUrl?: never +} | { + /** + * Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database. + */ + accelerateUrl: string + adapter?: never +}) & { + /** + * @default "colorless" + */ + errorFormat?: ErrorFormat + /** + * @example + * ``` + * // Shorthand for `emit: 'stdout'` + * log: ['query', 'info', 'warn', 'error'] + * + * // Emit as events only + * log: [ + * { emit: 'event', level: 'query' }, + * { emit: 'event', level: 'info' }, + * { emit: 'event', level: 'warn' } + * { emit: 'event', level: 'error' } + * ] + * + * / Emit as events and log to stdout + * og: [ + * { emit: 'stdout', level: 'query' }, + * { emit: 'stdout', level: 'info' }, + * { emit: 'stdout', level: 'warn' } + * { emit: 'stdout', level: 'error' } + * + * ``` + * Read more in our [docs](https://pris.ly/d/logging). + */ + log?: (LogLevel | LogDefinition)[] + /** + * The default values for transactionOptions + * maxWait ?= 2000 + * timeout ?= 5000 + */ + transactionOptions?: { + maxWait?: number + timeout?: number + isolationLevel?: TransactionIsolationLevel + } + /** + * Global configuration for omitting model fields by default. + * + * @example + * ``` + * const prisma = new PrismaClient({ + * omit: { + * user: { + * password: true + * } + * } + * }) + * ``` + */ + omit?: GlobalOmitConfig + /** + * SQL commenter plugins that add metadata to SQL queries as comments. + * Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/ + * + * @example + * ``` + * const prisma = new PrismaClient({ + * adapter, + * comments: [ + * traceContext(), + * queryInsights(), + * ], + * }) + * ``` + */ + comments?: runtime.SqlCommenterPlugin[] +} +export type GlobalOmitConfig = { + user?: Prisma.UserOmit + chat?: Prisma.ChatOmit + chatSession?: Prisma.ChatSessionOmit +} + +/* Types for Logging */ +export type LogLevel = 'info' | 'query' | 'warn' | 'error' +export type LogDefinition = { + level: LogLevel + emit: 'stdout' | 'event' +} + +export type CheckIsLogLevel = T extends LogLevel ? T : never; + +export type GetLogType = CheckIsLogLevel< + T extends LogDefinition ? T['level'] : T +>; + +export type GetEvents = T extends Array + ? GetLogType + : never; + +export type QueryEvent = { + timestamp: Date + query: string + params: string + duration: number + target: string +} + +export type LogEvent = { + timestamp: Date + message: string + target: string +} +/* End Types for Logging */ + + +export type PrismaAction = + | 'findUnique' + | 'findUniqueOrThrow' + | 'findMany' + | 'findFirst' + | 'findFirstOrThrow' + | 'create' + | 'createMany' + | 'createManyAndReturn' + | 'update' + | 'updateMany' + | 'updateManyAndReturn' + | 'upsert' + | 'delete' + | 'deleteMany' + | 'executeRaw' + | 'queryRaw' + | 'aggregate' + | 'count' + | 'runCommandRaw' + | 'findRaw' + | 'groupBy' + +/** + * `PrismaClient` proxy available in interactive transactions. + */ +export type TransactionClient = Omit + diff --git a/references/ai-chat/lib/generated/prisma/internal/prismaNamespaceBrowser.ts b/references/ai-chat/lib/generated/prisma/internal/prismaNamespaceBrowser.ts new file mode 100644 index 00000000000..e2ae15434e2 --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -0,0 +1,149 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file. + * While this enables partial backward compatibility, it is not part of the stable public API. + * + * If you are looking for your Models, Enums, and Input Types, please import them from the respective + * model files in the `model` directory! + */ + +import * as runtime from "@prisma/client/runtime/index-browser" + +export type * from '../models' +export type * from './prismaNamespace' + +export const Decimal = runtime.Decimal + + +export const NullTypes = { + DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull), + JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull), + AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull), +} +/** + * Helper for filtering JSON entries that have `null` on the database (empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const DbNull = runtime.DbNull + +/** + * Helper for filtering JSON entries that have JSON `null` values (not empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const JsonNull = runtime.JsonNull + +/** + * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull` + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const AnyNull = runtime.AnyNull + + +export const ModelName = { + User: 'User', + Chat: 'Chat', + ChatSession: 'ChatSession' +} as const + +export type ModelName = (typeof ModelName)[keyof typeof ModelName] + +/* + * Enums + */ + +export const TransactionIsolationLevel = runtime.makeStrictEnum({ + ReadUncommitted: 'ReadUncommitted', + ReadCommitted: 'ReadCommitted', + RepeatableRead: 'RepeatableRead', + Serializable: 'Serializable' +} as const) + +export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] + + +export const UserScalarFieldEnum = { + id: 'id', + name: 'name', + plan: 'plan', + preferredModel: 'preferredModel', + githubToken: 'githubToken', + messageCount: 'messageCount', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum] + + +export const ChatScalarFieldEnum = { + id: 'id', + title: 'title', + model: 'model', + messages: 'messages', + userId: 'userId', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type ChatScalarFieldEnum = (typeof ChatScalarFieldEnum)[keyof typeof ChatScalarFieldEnum] + + +export const ChatSessionScalarFieldEnum = { + id: 'id', + publicAccessToken: 'publicAccessToken', + lastEventId: 'lastEventId' +} as const + +export type ChatSessionScalarFieldEnum = (typeof ChatSessionScalarFieldEnum)[keyof typeof ChatSessionScalarFieldEnum] + + +export const SortOrder = { + asc: 'asc', + desc: 'desc' +} as const + +export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder] + + +export const JsonNullValueInput = { + JsonNull: JsonNull +} as const + +export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput] + + +export const QueryMode = { + default: 'default', + insensitive: 'insensitive' +} as const + +export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] + + +export const NullsOrder = { + first: 'first', + last: 'last' +} as const + +export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder] + + +export const JsonNullValueFilter = { + DbNull: DbNull, + JsonNull: JsonNull, + AnyNull: AnyNull +} as const + +export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter] + diff --git a/references/ai-chat/lib/generated/prisma/models.ts b/references/ai-chat/lib/generated/prisma/models.ts new file mode 100644 index 00000000000..e92f3b8e280 --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/models.ts @@ -0,0 +1,14 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This is a barrel export file for all models and their related types. + * + * 🟢 You can import this file directly. + */ +export type * from './models/User' +export type * from './models/Chat' +export type * from './models/ChatSession' +export type * from './commonInputTypes' \ No newline at end of file diff --git a/references/ai-chat/lib/generated/prisma/models/Chat.ts b/references/ai-chat/lib/generated/prisma/models/Chat.ts new file mode 100644 index 00000000000..3eab423a988 --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/models/Chat.ts @@ -0,0 +1,1423 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `Chat` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums" +import type * as Prisma from "../internal/prismaNamespace" + +/** + * Model Chat + * + */ +export type ChatModel = runtime.Types.Result.DefaultSelection + +export type AggregateChat = { + _count: ChatCountAggregateOutputType | null + _min: ChatMinAggregateOutputType | null + _max: ChatMaxAggregateOutputType | null +} + +export type ChatMinAggregateOutputType = { + id: string | null + title: string | null + model: string | null + userId: string | null + createdAt: Date | null + updatedAt: Date | null +} + +export type ChatMaxAggregateOutputType = { + id: string | null + title: string | null + model: string | null + userId: string | null + createdAt: Date | null + updatedAt: Date | null +} + +export type ChatCountAggregateOutputType = { + id: number + title: number + model: number + messages: number + userId: number + createdAt: number + updatedAt: number + _all: number +} + + +export type ChatMinAggregateInputType = { + id?: true + title?: true + model?: true + userId?: true + createdAt?: true + updatedAt?: true +} + +export type ChatMaxAggregateInputType = { + id?: true + title?: true + model?: true + userId?: true + createdAt?: true + updatedAt?: true +} + +export type ChatCountAggregateInputType = { + id?: true + title?: true + model?: true + messages?: true + userId?: true + createdAt?: true + updatedAt?: true + _all?: true +} + +export type ChatAggregateArgs = { + /** + * Filter which Chat to aggregate. + */ + where?: Prisma.ChatWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Chats to fetch. + */ + orderBy?: Prisma.ChatOrderByWithRelationInput | Prisma.ChatOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.ChatWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Chats from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Chats. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Chats + **/ + _count?: true | ChatCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: ChatMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: ChatMaxAggregateInputType +} + +export type GetChatAggregateType = { + [P in keyof T & keyof AggregateChat]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type ChatGroupByArgs = { + where?: Prisma.ChatWhereInput + orderBy?: Prisma.ChatOrderByWithAggregationInput | Prisma.ChatOrderByWithAggregationInput[] + by: Prisma.ChatScalarFieldEnum[] | Prisma.ChatScalarFieldEnum + having?: Prisma.ChatScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: ChatCountAggregateInputType | true + _min?: ChatMinAggregateInputType + _max?: ChatMaxAggregateInputType +} + +export type ChatGroupByOutputType = { + id: string + title: string + model: string + messages: runtime.JsonValue + userId: string | null + createdAt: Date + updatedAt: Date + _count: ChatCountAggregateOutputType | null + _min: ChatMinAggregateOutputType | null + _max: ChatMaxAggregateOutputType | null +} + +type GetChatGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof ChatGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type ChatWhereInput = { + AND?: Prisma.ChatWhereInput | Prisma.ChatWhereInput[] + OR?: Prisma.ChatWhereInput[] + NOT?: Prisma.ChatWhereInput | Prisma.ChatWhereInput[] + id?: Prisma.StringFilter<"Chat"> | string + title?: Prisma.StringFilter<"Chat"> | string + model?: Prisma.StringFilter<"Chat"> | string + messages?: Prisma.JsonFilter<"Chat"> + userId?: Prisma.StringNullableFilter<"Chat"> | string | null + createdAt?: Prisma.DateTimeFilter<"Chat"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"Chat"> | Date | string + user?: Prisma.XOR | null +} + +export type ChatOrderByWithRelationInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + model?: Prisma.SortOrder + messages?: Prisma.SortOrder + userId?: Prisma.SortOrderInput | Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder + user?: Prisma.UserOrderByWithRelationInput +} + +export type ChatWhereUniqueInput = Prisma.AtLeast<{ + id?: string + AND?: Prisma.ChatWhereInput | Prisma.ChatWhereInput[] + OR?: Prisma.ChatWhereInput[] + NOT?: Prisma.ChatWhereInput | Prisma.ChatWhereInput[] + title?: Prisma.StringFilter<"Chat"> | string + model?: Prisma.StringFilter<"Chat"> | string + messages?: Prisma.JsonFilter<"Chat"> + userId?: Prisma.StringNullableFilter<"Chat"> | string | null + createdAt?: Prisma.DateTimeFilter<"Chat"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"Chat"> | Date | string + user?: Prisma.XOR | null +}, "id"> + +export type ChatOrderByWithAggregationInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + model?: Prisma.SortOrder + messages?: Prisma.SortOrder + userId?: Prisma.SortOrderInput | Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder + _count?: Prisma.ChatCountOrderByAggregateInput + _max?: Prisma.ChatMaxOrderByAggregateInput + _min?: Prisma.ChatMinOrderByAggregateInput +} + +export type ChatScalarWhereWithAggregatesInput = { + AND?: Prisma.ChatScalarWhereWithAggregatesInput | Prisma.ChatScalarWhereWithAggregatesInput[] + OR?: Prisma.ChatScalarWhereWithAggregatesInput[] + NOT?: Prisma.ChatScalarWhereWithAggregatesInput | Prisma.ChatScalarWhereWithAggregatesInput[] + id?: Prisma.StringWithAggregatesFilter<"Chat"> | string + title?: Prisma.StringWithAggregatesFilter<"Chat"> | string + model?: Prisma.StringWithAggregatesFilter<"Chat"> | string + messages?: Prisma.JsonWithAggregatesFilter<"Chat"> + userId?: Prisma.StringNullableWithAggregatesFilter<"Chat"> | string | null + createdAt?: Prisma.DateTimeWithAggregatesFilter<"Chat"> | Date | string + updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Chat"> | Date | string +} + +export type ChatCreateInput = { + id: string + title: string + model?: string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Date | string + updatedAt?: Date | string + user?: Prisma.UserCreateNestedOneWithoutChatsInput +} + +export type ChatUncheckedCreateInput = { + id: string + title: string + model?: string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + userId?: string | null + createdAt?: Date | string + updatedAt?: Date | string +} + +export type ChatUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + title?: Prisma.StringFieldUpdateOperationsInput | string + model?: Prisma.StringFieldUpdateOperationsInput | string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + user?: Prisma.UserUpdateOneWithoutChatsNestedInput +} + +export type ChatUncheckedUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + title?: Prisma.StringFieldUpdateOperationsInput | string + model?: Prisma.StringFieldUpdateOperationsInput | string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type ChatCreateManyInput = { + id: string + title: string + model?: string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + userId?: string | null + createdAt?: Date | string + updatedAt?: Date | string +} + +export type ChatUpdateManyMutationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + title?: Prisma.StringFieldUpdateOperationsInput | string + model?: Prisma.StringFieldUpdateOperationsInput | string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type ChatUncheckedUpdateManyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + title?: Prisma.StringFieldUpdateOperationsInput | string + model?: Prisma.StringFieldUpdateOperationsInput | string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + userId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type ChatListRelationFilter = { + every?: Prisma.ChatWhereInput + some?: Prisma.ChatWhereInput + none?: Prisma.ChatWhereInput +} + +export type ChatOrderByRelationAggregateInput = { + _count?: Prisma.SortOrder +} + +export type ChatCountOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + model?: Prisma.SortOrder + messages?: Prisma.SortOrder + userId?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type ChatMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + model?: Prisma.SortOrder + userId?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type ChatMinOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + model?: Prisma.SortOrder + userId?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type ChatCreateNestedManyWithoutUserInput = { + create?: Prisma.XOR | Prisma.ChatCreateWithoutUserInput[] | Prisma.ChatUncheckedCreateWithoutUserInput[] + connectOrCreate?: Prisma.ChatCreateOrConnectWithoutUserInput | Prisma.ChatCreateOrConnectWithoutUserInput[] + createMany?: Prisma.ChatCreateManyUserInputEnvelope + connect?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] +} + +export type ChatUncheckedCreateNestedManyWithoutUserInput = { + create?: Prisma.XOR | Prisma.ChatCreateWithoutUserInput[] | Prisma.ChatUncheckedCreateWithoutUserInput[] + connectOrCreate?: Prisma.ChatCreateOrConnectWithoutUserInput | Prisma.ChatCreateOrConnectWithoutUserInput[] + createMany?: Prisma.ChatCreateManyUserInputEnvelope + connect?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] +} + +export type ChatUpdateManyWithoutUserNestedInput = { + create?: Prisma.XOR | Prisma.ChatCreateWithoutUserInput[] | Prisma.ChatUncheckedCreateWithoutUserInput[] + connectOrCreate?: Prisma.ChatCreateOrConnectWithoutUserInput | Prisma.ChatCreateOrConnectWithoutUserInput[] + upsert?: Prisma.ChatUpsertWithWhereUniqueWithoutUserInput | Prisma.ChatUpsertWithWhereUniqueWithoutUserInput[] + createMany?: Prisma.ChatCreateManyUserInputEnvelope + set?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] + disconnect?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] + delete?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] + connect?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] + update?: Prisma.ChatUpdateWithWhereUniqueWithoutUserInput | Prisma.ChatUpdateWithWhereUniqueWithoutUserInput[] + updateMany?: Prisma.ChatUpdateManyWithWhereWithoutUserInput | Prisma.ChatUpdateManyWithWhereWithoutUserInput[] + deleteMany?: Prisma.ChatScalarWhereInput | Prisma.ChatScalarWhereInput[] +} + +export type ChatUncheckedUpdateManyWithoutUserNestedInput = { + create?: Prisma.XOR | Prisma.ChatCreateWithoutUserInput[] | Prisma.ChatUncheckedCreateWithoutUserInput[] + connectOrCreate?: Prisma.ChatCreateOrConnectWithoutUserInput | Prisma.ChatCreateOrConnectWithoutUserInput[] + upsert?: Prisma.ChatUpsertWithWhereUniqueWithoutUserInput | Prisma.ChatUpsertWithWhereUniqueWithoutUserInput[] + createMany?: Prisma.ChatCreateManyUserInputEnvelope + set?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] + disconnect?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] + delete?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] + connect?: Prisma.ChatWhereUniqueInput | Prisma.ChatWhereUniqueInput[] + update?: Prisma.ChatUpdateWithWhereUniqueWithoutUserInput | Prisma.ChatUpdateWithWhereUniqueWithoutUserInput[] + updateMany?: Prisma.ChatUpdateManyWithWhereWithoutUserInput | Prisma.ChatUpdateManyWithWhereWithoutUserInput[] + deleteMany?: Prisma.ChatScalarWhereInput | Prisma.ChatScalarWhereInput[] +} + +export type ChatCreateWithoutUserInput = { + id: string + title: string + model?: string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Date | string + updatedAt?: Date | string +} + +export type ChatUncheckedCreateWithoutUserInput = { + id: string + title: string + model?: string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Date | string + updatedAt?: Date | string +} + +export type ChatCreateOrConnectWithoutUserInput = { + where: Prisma.ChatWhereUniqueInput + create: Prisma.XOR +} + +export type ChatCreateManyUserInputEnvelope = { + data: Prisma.ChatCreateManyUserInput | Prisma.ChatCreateManyUserInput[] + skipDuplicates?: boolean +} + +export type ChatUpsertWithWhereUniqueWithoutUserInput = { + where: Prisma.ChatWhereUniqueInput + update: Prisma.XOR + create: Prisma.XOR +} + +export type ChatUpdateWithWhereUniqueWithoutUserInput = { + where: Prisma.ChatWhereUniqueInput + data: Prisma.XOR +} + +export type ChatUpdateManyWithWhereWithoutUserInput = { + where: Prisma.ChatScalarWhereInput + data: Prisma.XOR +} + +export type ChatScalarWhereInput = { + AND?: Prisma.ChatScalarWhereInput | Prisma.ChatScalarWhereInput[] + OR?: Prisma.ChatScalarWhereInput[] + NOT?: Prisma.ChatScalarWhereInput | Prisma.ChatScalarWhereInput[] + id?: Prisma.StringFilter<"Chat"> | string + title?: Prisma.StringFilter<"Chat"> | string + model?: Prisma.StringFilter<"Chat"> | string + messages?: Prisma.JsonFilter<"Chat"> + userId?: Prisma.StringNullableFilter<"Chat"> | string | null + createdAt?: Prisma.DateTimeFilter<"Chat"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"Chat"> | Date | string +} + +export type ChatCreateManyUserInput = { + id: string + title: string + model?: string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Date | string + updatedAt?: Date | string +} + +export type ChatUpdateWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + title?: Prisma.StringFieldUpdateOperationsInput | string + model?: Prisma.StringFieldUpdateOperationsInput | string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type ChatUncheckedUpdateWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + title?: Prisma.StringFieldUpdateOperationsInput | string + model?: Prisma.StringFieldUpdateOperationsInput | string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type ChatUncheckedUpdateManyWithoutUserInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + title?: Prisma.StringFieldUpdateOperationsInput | string + model?: Prisma.StringFieldUpdateOperationsInput | string + messages?: Prisma.JsonNullValueInput | runtime.InputJsonValue + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + + + +export type ChatSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + model?: boolean + messages?: boolean + userId?: boolean + createdAt?: boolean + updatedAt?: boolean + user?: boolean | Prisma.Chat$userArgs +}, ExtArgs["result"]["chat"]> + +export type ChatSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + model?: boolean + messages?: boolean + userId?: boolean + createdAt?: boolean + updatedAt?: boolean + user?: boolean | Prisma.Chat$userArgs +}, ExtArgs["result"]["chat"]> + +export type ChatSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + model?: boolean + messages?: boolean + userId?: boolean + createdAt?: boolean + updatedAt?: boolean + user?: boolean | Prisma.Chat$userArgs +}, ExtArgs["result"]["chat"]> + +export type ChatSelectScalar = { + id?: boolean + title?: boolean + model?: boolean + messages?: boolean + userId?: boolean + createdAt?: boolean + updatedAt?: boolean +} + +export type ChatOmit = runtime.Types.Extensions.GetOmit<"id" | "title" | "model" | "messages" | "userId" | "createdAt" | "updatedAt", ExtArgs["result"]["chat"]> +export type ChatInclude = { + user?: boolean | Prisma.Chat$userArgs +} +export type ChatIncludeCreateManyAndReturn = { + user?: boolean | Prisma.Chat$userArgs +} +export type ChatIncludeUpdateManyAndReturn = { + user?: boolean | Prisma.Chat$userArgs +} + +export type $ChatPayload = { + name: "Chat" + objects: { + user: Prisma.$UserPayload | null + } + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: string + title: string + model: string + messages: runtime.JsonValue + userId: string | null + createdAt: Date + updatedAt: Date + }, ExtArgs["result"]["chat"]> + composites: {} +} + +export type ChatGetPayload = runtime.Types.Result.GetResult + +export type ChatCountArgs = + Omit & { + select?: ChatCountAggregateInputType | true + } + +export interface ChatDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['Chat'], meta: { name: 'Chat' } } + /** + * Find zero or one Chat that matches the filter. + * @param {ChatFindUniqueArgs} args - Arguments to find a Chat + * @example + * // Get one Chat + * const chat = await prisma.chat.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__ChatClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one Chat that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {ChatFindUniqueOrThrowArgs} args - Arguments to find a Chat + * @example + * // Get one Chat + * const chat = await prisma.chat.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__ChatClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Chat that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatFindFirstArgs} args - Arguments to find a Chat + * @example + * // Get one Chat + * const chat = await prisma.chat.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__ChatClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Chat that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatFindFirstOrThrowArgs} args - Arguments to find a Chat + * @example + * // Get one Chat + * const chat = await prisma.chat.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__ChatClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more Chats that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all Chats + * const chats = await prisma.chat.findMany() + * + * // Get first 10 Chats + * const chats = await prisma.chat.findMany({ take: 10 }) + * + * // Only select the `id` + * const chatWithIdOnly = await prisma.chat.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a Chat. + * @param {ChatCreateArgs} args - Arguments to create a Chat. + * @example + * // Create one Chat + * const Chat = await prisma.chat.create({ + * data: { + * // ... data to create a Chat + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__ChatClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many Chats. + * @param {ChatCreateManyArgs} args - Arguments to create many Chats. + * @example + * // Create many Chats + * const chat = await prisma.chat.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many Chats and returns the data saved in the database. + * @param {ChatCreateManyAndReturnArgs} args - Arguments to create many Chats. + * @example + * // Create many Chats + * const chat = await prisma.chat.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many Chats and only return the `id` + * const chatWithIdOnly = await prisma.chat.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a Chat. + * @param {ChatDeleteArgs} args - Arguments to delete one Chat. + * @example + * // Delete one Chat + * const Chat = await prisma.chat.delete({ + * where: { + * // ... filter to delete one Chat + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__ChatClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one Chat. + * @param {ChatUpdateArgs} args - Arguments to update one Chat. + * @example + * // Update one Chat + * const chat = await prisma.chat.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__ChatClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more Chats. + * @param {ChatDeleteManyArgs} args - Arguments to filter Chats to delete. + * @example + * // Delete a few Chats + * const { count } = await prisma.chat.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Chats. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Chats + * const chat = await prisma.chat.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Chats and returns the data updated in the database. + * @param {ChatUpdateManyAndReturnArgs} args - Arguments to update many Chats. + * @example + * // Update many Chats + * const chat = await prisma.chat.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more Chats and only return the `id` + * const chatWithIdOnly = await prisma.chat.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one Chat. + * @param {ChatUpsertArgs} args - Arguments to update or create a Chat. + * @example + * // Update or create a Chat + * const chat = await prisma.chat.upsert({ + * create: { + * // ... data to create a Chat + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the Chat we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__ChatClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of Chats. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatCountArgs} args - Arguments to filter Chats to count. + * @example + * // Count the number of Chats + * const count = await prisma.chat.count({ + * where: { + * // ... the filter for the Chats we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a Chat. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by Chat. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends ChatGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: ChatGroupByArgs['orderBy'] } + : { orderBy?: ChatGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetChatGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the Chat model + */ +readonly fields: ChatFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for Chat. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__ChatClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + user = {}>(args?: Prisma.Subset>): Prisma.Prisma__UserClient, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the Chat model + */ +export interface ChatFieldRefs { + readonly id: Prisma.FieldRef<"Chat", 'String'> + readonly title: Prisma.FieldRef<"Chat", 'String'> + readonly model: Prisma.FieldRef<"Chat", 'String'> + readonly messages: Prisma.FieldRef<"Chat", 'Json'> + readonly userId: Prisma.FieldRef<"Chat", 'String'> + readonly createdAt: Prisma.FieldRef<"Chat", 'DateTime'> + readonly updatedAt: Prisma.FieldRef<"Chat", 'DateTime'> +} + + +// Custom InputTypes +/** + * Chat findUnique + */ +export type ChatFindUniqueArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * Filter, which Chat to fetch. + */ + where: Prisma.ChatWhereUniqueInput +} + +/** + * Chat findUniqueOrThrow + */ +export type ChatFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * Filter, which Chat to fetch. + */ + where: Prisma.ChatWhereUniqueInput +} + +/** + * Chat findFirst + */ +export type ChatFindFirstArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * Filter, which Chat to fetch. + */ + where?: Prisma.ChatWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Chats to fetch. + */ + orderBy?: Prisma.ChatOrderByWithRelationInput | Prisma.ChatOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Chats. + */ + cursor?: Prisma.ChatWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Chats from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Chats. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Chats. + */ + distinct?: Prisma.ChatScalarFieldEnum | Prisma.ChatScalarFieldEnum[] +} + +/** + * Chat findFirstOrThrow + */ +export type ChatFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * Filter, which Chat to fetch. + */ + where?: Prisma.ChatWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Chats to fetch. + */ + orderBy?: Prisma.ChatOrderByWithRelationInput | Prisma.ChatOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Chats. + */ + cursor?: Prisma.ChatWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Chats from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Chats. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Chats. + */ + distinct?: Prisma.ChatScalarFieldEnum | Prisma.ChatScalarFieldEnum[] +} + +/** + * Chat findMany + */ +export type ChatFindManyArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * Filter, which Chats to fetch. + */ + where?: Prisma.ChatWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Chats to fetch. + */ + orderBy?: Prisma.ChatOrderByWithRelationInput | Prisma.ChatOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Chats. + */ + cursor?: Prisma.ChatWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Chats from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Chats. + */ + skip?: number + distinct?: Prisma.ChatScalarFieldEnum | Prisma.ChatScalarFieldEnum[] +} + +/** + * Chat create + */ +export type ChatCreateArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * The data needed to create a Chat. + */ + data: Prisma.XOR +} + +/** + * Chat createMany + */ +export type ChatCreateManyArgs = { + /** + * The data used to create many Chats. + */ + data: Prisma.ChatCreateManyInput | Prisma.ChatCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Chat createManyAndReturn + */ +export type ChatCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelectCreateManyAndReturn | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * The data used to create many Chats. + */ + data: Prisma.ChatCreateManyInput | Prisma.ChatCreateManyInput[] + skipDuplicates?: boolean + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatIncludeCreateManyAndReturn | null +} + +/** + * Chat update + */ +export type ChatUpdateArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * The data needed to update a Chat. + */ + data: Prisma.XOR + /** + * Choose, which Chat to update. + */ + where: Prisma.ChatWhereUniqueInput +} + +/** + * Chat updateMany + */ +export type ChatUpdateManyArgs = { + /** + * The data used to update Chats. + */ + data: Prisma.XOR + /** + * Filter which Chats to update + */ + where?: Prisma.ChatWhereInput + /** + * Limit how many Chats to update. + */ + limit?: number +} + +/** + * Chat updateManyAndReturn + */ +export type ChatUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * The data used to update Chats. + */ + data: Prisma.XOR + /** + * Filter which Chats to update + */ + where?: Prisma.ChatWhereInput + /** + * Limit how many Chats to update. + */ + limit?: number + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatIncludeUpdateManyAndReturn | null +} + +/** + * Chat upsert + */ +export type ChatUpsertArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * The filter to search for the Chat to update in case it exists. + */ + where: Prisma.ChatWhereUniqueInput + /** + * In case the Chat found by the `where` argument doesn't exist, create a new Chat with this data. + */ + create: Prisma.XOR + /** + * In case the Chat was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * Chat delete + */ +export type ChatDeleteArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + /** + * Filter which Chat to delete. + */ + where: Prisma.ChatWhereUniqueInput +} + +/** + * Chat deleteMany + */ +export type ChatDeleteManyArgs = { + /** + * Filter which Chats to delete + */ + where?: Prisma.ChatWhereInput + /** + * Limit how many Chats to delete. + */ + limit?: number +} + +/** + * Chat.user + */ +export type Chat$userArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + where?: Prisma.UserWhereInput +} + +/** + * Chat without action + */ +export type ChatDefaultArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null +} diff --git a/references/ai-chat/lib/generated/prisma/models/ChatSession.ts b/references/ai-chat/lib/generated/prisma/models/ChatSession.ts new file mode 100644 index 00000000000..0d40318b22e --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/models/ChatSession.ts @@ -0,0 +1,1088 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `ChatSession` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums" +import type * as Prisma from "../internal/prismaNamespace" + +/** + * Model ChatSession + * + */ +export type ChatSessionModel = runtime.Types.Result.DefaultSelection + +export type AggregateChatSession = { + _count: ChatSessionCountAggregateOutputType | null + _min: ChatSessionMinAggregateOutputType | null + _max: ChatSessionMaxAggregateOutputType | null +} + +export type ChatSessionMinAggregateOutputType = { + id: string | null + publicAccessToken: string | null + lastEventId: string | null +} + +export type ChatSessionMaxAggregateOutputType = { + id: string | null + publicAccessToken: string | null + lastEventId: string | null +} + +export type ChatSessionCountAggregateOutputType = { + id: number + publicAccessToken: number + lastEventId: number + _all: number +} + + +export type ChatSessionMinAggregateInputType = { + id?: true + publicAccessToken?: true + lastEventId?: true +} + +export type ChatSessionMaxAggregateInputType = { + id?: true + publicAccessToken?: true + lastEventId?: true +} + +export type ChatSessionCountAggregateInputType = { + id?: true + publicAccessToken?: true + lastEventId?: true + _all?: true +} + +export type ChatSessionAggregateArgs = { + /** + * Filter which ChatSession to aggregate. + */ + where?: Prisma.ChatSessionWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of ChatSessions to fetch. + */ + orderBy?: Prisma.ChatSessionOrderByWithRelationInput | Prisma.ChatSessionOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.ChatSessionWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` ChatSessions from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` ChatSessions. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned ChatSessions + **/ + _count?: true | ChatSessionCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: ChatSessionMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: ChatSessionMaxAggregateInputType +} + +export type GetChatSessionAggregateType = { + [P in keyof T & keyof AggregateChatSession]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type ChatSessionGroupByArgs = { + where?: Prisma.ChatSessionWhereInput + orderBy?: Prisma.ChatSessionOrderByWithAggregationInput | Prisma.ChatSessionOrderByWithAggregationInput[] + by: Prisma.ChatSessionScalarFieldEnum[] | Prisma.ChatSessionScalarFieldEnum + having?: Prisma.ChatSessionScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: ChatSessionCountAggregateInputType | true + _min?: ChatSessionMinAggregateInputType + _max?: ChatSessionMaxAggregateInputType +} + +export type ChatSessionGroupByOutputType = { + id: string + publicAccessToken: string + lastEventId: string | null + _count: ChatSessionCountAggregateOutputType | null + _min: ChatSessionMinAggregateOutputType | null + _max: ChatSessionMaxAggregateOutputType | null +} + +type GetChatSessionGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof ChatSessionGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type ChatSessionWhereInput = { + AND?: Prisma.ChatSessionWhereInput | Prisma.ChatSessionWhereInput[] + OR?: Prisma.ChatSessionWhereInput[] + NOT?: Prisma.ChatSessionWhereInput | Prisma.ChatSessionWhereInput[] + id?: Prisma.StringFilter<"ChatSession"> | string + publicAccessToken?: Prisma.StringFilter<"ChatSession"> | string + lastEventId?: Prisma.StringNullableFilter<"ChatSession"> | string | null +} + +export type ChatSessionOrderByWithRelationInput = { + id?: Prisma.SortOrder + publicAccessToken?: Prisma.SortOrder + lastEventId?: Prisma.SortOrderInput | Prisma.SortOrder +} + +export type ChatSessionWhereUniqueInput = Prisma.AtLeast<{ + id?: string + AND?: Prisma.ChatSessionWhereInput | Prisma.ChatSessionWhereInput[] + OR?: Prisma.ChatSessionWhereInput[] + NOT?: Prisma.ChatSessionWhereInput | Prisma.ChatSessionWhereInput[] + publicAccessToken?: Prisma.StringFilter<"ChatSession"> | string + lastEventId?: Prisma.StringNullableFilter<"ChatSession"> | string | null +}, "id"> + +export type ChatSessionOrderByWithAggregationInput = { + id?: Prisma.SortOrder + publicAccessToken?: Prisma.SortOrder + lastEventId?: Prisma.SortOrderInput | Prisma.SortOrder + _count?: Prisma.ChatSessionCountOrderByAggregateInput + _max?: Prisma.ChatSessionMaxOrderByAggregateInput + _min?: Prisma.ChatSessionMinOrderByAggregateInput +} + +export type ChatSessionScalarWhereWithAggregatesInput = { + AND?: Prisma.ChatSessionScalarWhereWithAggregatesInput | Prisma.ChatSessionScalarWhereWithAggregatesInput[] + OR?: Prisma.ChatSessionScalarWhereWithAggregatesInput[] + NOT?: Prisma.ChatSessionScalarWhereWithAggregatesInput | Prisma.ChatSessionScalarWhereWithAggregatesInput[] + id?: Prisma.StringWithAggregatesFilter<"ChatSession"> | string + publicAccessToken?: Prisma.StringWithAggregatesFilter<"ChatSession"> | string + lastEventId?: Prisma.StringNullableWithAggregatesFilter<"ChatSession"> | string | null +} + +export type ChatSessionCreateInput = { + id: string + publicAccessToken: string + lastEventId?: string | null +} + +export type ChatSessionUncheckedCreateInput = { + id: string + publicAccessToken: string + lastEventId?: string | null +} + +export type ChatSessionUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + publicAccessToken?: Prisma.StringFieldUpdateOperationsInput | string + lastEventId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + +export type ChatSessionUncheckedUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + publicAccessToken?: Prisma.StringFieldUpdateOperationsInput | string + lastEventId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + +export type ChatSessionCreateManyInput = { + id: string + publicAccessToken: string + lastEventId?: string | null +} + +export type ChatSessionUpdateManyMutationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + publicAccessToken?: Prisma.StringFieldUpdateOperationsInput | string + lastEventId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + +export type ChatSessionUncheckedUpdateManyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + publicAccessToken?: Prisma.StringFieldUpdateOperationsInput | string + lastEventId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + +export type ChatSessionCountOrderByAggregateInput = { + id?: Prisma.SortOrder + publicAccessToken?: Prisma.SortOrder + lastEventId?: Prisma.SortOrder +} + +export type ChatSessionMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + publicAccessToken?: Prisma.SortOrder + lastEventId?: Prisma.SortOrder +} + +export type ChatSessionMinOrderByAggregateInput = { + id?: Prisma.SortOrder + publicAccessToken?: Prisma.SortOrder + lastEventId?: Prisma.SortOrder +} + + + +export type ChatSessionSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + publicAccessToken?: boolean + lastEventId?: boolean +}, ExtArgs["result"]["chatSession"]> + +export type ChatSessionSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + publicAccessToken?: boolean + lastEventId?: boolean +}, ExtArgs["result"]["chatSession"]> + +export type ChatSessionSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + publicAccessToken?: boolean + lastEventId?: boolean +}, ExtArgs["result"]["chatSession"]> + +export type ChatSessionSelectScalar = { + id?: boolean + publicAccessToken?: boolean + lastEventId?: boolean +} + +export type ChatSessionOmit = runtime.Types.Extensions.GetOmit<"id" | "publicAccessToken" | "lastEventId", ExtArgs["result"]["chatSession"]> + +export type $ChatSessionPayload = { + name: "ChatSession" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: string + publicAccessToken: string + lastEventId: string | null + }, ExtArgs["result"]["chatSession"]> + composites: {} +} + +export type ChatSessionGetPayload = runtime.Types.Result.GetResult + +export type ChatSessionCountArgs = + Omit & { + select?: ChatSessionCountAggregateInputType | true + } + +export interface ChatSessionDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['ChatSession'], meta: { name: 'ChatSession' } } + /** + * Find zero or one ChatSession that matches the filter. + * @param {ChatSessionFindUniqueArgs} args - Arguments to find a ChatSession + * @example + * // Get one ChatSession + * const chatSession = await prisma.chatSession.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__ChatSessionClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one ChatSession that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {ChatSessionFindUniqueOrThrowArgs} args - Arguments to find a ChatSession + * @example + * // Get one ChatSession + * const chatSession = await prisma.chatSession.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__ChatSessionClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first ChatSession that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatSessionFindFirstArgs} args - Arguments to find a ChatSession + * @example + * // Get one ChatSession + * const chatSession = await prisma.chatSession.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__ChatSessionClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first ChatSession that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatSessionFindFirstOrThrowArgs} args - Arguments to find a ChatSession + * @example + * // Get one ChatSession + * const chatSession = await prisma.chatSession.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__ChatSessionClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more ChatSessions that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatSessionFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all ChatSessions + * const chatSessions = await prisma.chatSession.findMany() + * + * // Get first 10 ChatSessions + * const chatSessions = await prisma.chatSession.findMany({ take: 10 }) + * + * // Only select the `id` + * const chatSessionWithIdOnly = await prisma.chatSession.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a ChatSession. + * @param {ChatSessionCreateArgs} args - Arguments to create a ChatSession. + * @example + * // Create one ChatSession + * const ChatSession = await prisma.chatSession.create({ + * data: { + * // ... data to create a ChatSession + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__ChatSessionClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many ChatSessions. + * @param {ChatSessionCreateManyArgs} args - Arguments to create many ChatSessions. + * @example + * // Create many ChatSessions + * const chatSession = await prisma.chatSession.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many ChatSessions and returns the data saved in the database. + * @param {ChatSessionCreateManyAndReturnArgs} args - Arguments to create many ChatSessions. + * @example + * // Create many ChatSessions + * const chatSession = await prisma.chatSession.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many ChatSessions and only return the `id` + * const chatSessionWithIdOnly = await prisma.chatSession.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a ChatSession. + * @param {ChatSessionDeleteArgs} args - Arguments to delete one ChatSession. + * @example + * // Delete one ChatSession + * const ChatSession = await prisma.chatSession.delete({ + * where: { + * // ... filter to delete one ChatSession + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__ChatSessionClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one ChatSession. + * @param {ChatSessionUpdateArgs} args - Arguments to update one ChatSession. + * @example + * // Update one ChatSession + * const chatSession = await prisma.chatSession.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__ChatSessionClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more ChatSessions. + * @param {ChatSessionDeleteManyArgs} args - Arguments to filter ChatSessions to delete. + * @example + * // Delete a few ChatSessions + * const { count } = await prisma.chatSession.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more ChatSessions. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatSessionUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many ChatSessions + * const chatSession = await prisma.chatSession.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more ChatSessions and returns the data updated in the database. + * @param {ChatSessionUpdateManyAndReturnArgs} args - Arguments to update many ChatSessions. + * @example + * // Update many ChatSessions + * const chatSession = await prisma.chatSession.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more ChatSessions and only return the `id` + * const chatSessionWithIdOnly = await prisma.chatSession.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one ChatSession. + * @param {ChatSessionUpsertArgs} args - Arguments to update or create a ChatSession. + * @example + * // Update or create a ChatSession + * const chatSession = await prisma.chatSession.upsert({ + * create: { + * // ... data to create a ChatSession + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the ChatSession we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__ChatSessionClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of ChatSessions. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatSessionCountArgs} args - Arguments to filter ChatSessions to count. + * @example + * // Count the number of ChatSessions + * const count = await prisma.chatSession.count({ + * where: { + * // ... the filter for the ChatSessions we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a ChatSession. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatSessionAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by ChatSession. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ChatSessionGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends ChatSessionGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: ChatSessionGroupByArgs['orderBy'] } + : { orderBy?: ChatSessionGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetChatSessionGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the ChatSession model + */ +readonly fields: ChatSessionFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for ChatSession. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__ChatSessionClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the ChatSession model + */ +export interface ChatSessionFieldRefs { + readonly id: Prisma.FieldRef<"ChatSession", 'String'> + readonly publicAccessToken: Prisma.FieldRef<"ChatSession", 'String'> + readonly lastEventId: Prisma.FieldRef<"ChatSession", 'String'> +} + + +// Custom InputTypes +/** + * ChatSession findUnique + */ +export type ChatSessionFindUniqueArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * Filter, which ChatSession to fetch. + */ + where: Prisma.ChatSessionWhereUniqueInput +} + +/** + * ChatSession findUniqueOrThrow + */ +export type ChatSessionFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * Filter, which ChatSession to fetch. + */ + where: Prisma.ChatSessionWhereUniqueInput +} + +/** + * ChatSession findFirst + */ +export type ChatSessionFindFirstArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * Filter, which ChatSession to fetch. + */ + where?: Prisma.ChatSessionWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of ChatSessions to fetch. + */ + orderBy?: Prisma.ChatSessionOrderByWithRelationInput | Prisma.ChatSessionOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for ChatSessions. + */ + cursor?: Prisma.ChatSessionWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` ChatSessions from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` ChatSessions. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of ChatSessions. + */ + distinct?: Prisma.ChatSessionScalarFieldEnum | Prisma.ChatSessionScalarFieldEnum[] +} + +/** + * ChatSession findFirstOrThrow + */ +export type ChatSessionFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * Filter, which ChatSession to fetch. + */ + where?: Prisma.ChatSessionWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of ChatSessions to fetch. + */ + orderBy?: Prisma.ChatSessionOrderByWithRelationInput | Prisma.ChatSessionOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for ChatSessions. + */ + cursor?: Prisma.ChatSessionWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` ChatSessions from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` ChatSessions. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of ChatSessions. + */ + distinct?: Prisma.ChatSessionScalarFieldEnum | Prisma.ChatSessionScalarFieldEnum[] +} + +/** + * ChatSession findMany + */ +export type ChatSessionFindManyArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * Filter, which ChatSessions to fetch. + */ + where?: Prisma.ChatSessionWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of ChatSessions to fetch. + */ + orderBy?: Prisma.ChatSessionOrderByWithRelationInput | Prisma.ChatSessionOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing ChatSessions. + */ + cursor?: Prisma.ChatSessionWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` ChatSessions from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` ChatSessions. + */ + skip?: number + distinct?: Prisma.ChatSessionScalarFieldEnum | Prisma.ChatSessionScalarFieldEnum[] +} + +/** + * ChatSession create + */ +export type ChatSessionCreateArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * The data needed to create a ChatSession. + */ + data: Prisma.XOR +} + +/** + * ChatSession createMany + */ +export type ChatSessionCreateManyArgs = { + /** + * The data used to create many ChatSessions. + */ + data: Prisma.ChatSessionCreateManyInput | Prisma.ChatSessionCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * ChatSession createManyAndReturn + */ +export type ChatSessionCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelectCreateManyAndReturn | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * The data used to create many ChatSessions. + */ + data: Prisma.ChatSessionCreateManyInput | Prisma.ChatSessionCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * ChatSession update + */ +export type ChatSessionUpdateArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * The data needed to update a ChatSession. + */ + data: Prisma.XOR + /** + * Choose, which ChatSession to update. + */ + where: Prisma.ChatSessionWhereUniqueInput +} + +/** + * ChatSession updateMany + */ +export type ChatSessionUpdateManyArgs = { + /** + * The data used to update ChatSessions. + */ + data: Prisma.XOR + /** + * Filter which ChatSessions to update + */ + where?: Prisma.ChatSessionWhereInput + /** + * Limit how many ChatSessions to update. + */ + limit?: number +} + +/** + * ChatSession updateManyAndReturn + */ +export type ChatSessionUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * The data used to update ChatSessions. + */ + data: Prisma.XOR + /** + * Filter which ChatSessions to update + */ + where?: Prisma.ChatSessionWhereInput + /** + * Limit how many ChatSessions to update. + */ + limit?: number +} + +/** + * ChatSession upsert + */ +export type ChatSessionUpsertArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * The filter to search for the ChatSession to update in case it exists. + */ + where: Prisma.ChatSessionWhereUniqueInput + /** + * In case the ChatSession found by the `where` argument doesn't exist, create a new ChatSession with this data. + */ + create: Prisma.XOR + /** + * In case the ChatSession was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * ChatSession delete + */ +export type ChatSessionDeleteArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null + /** + * Filter which ChatSession to delete. + */ + where: Prisma.ChatSessionWhereUniqueInput +} + +/** + * ChatSession deleteMany + */ +export type ChatSessionDeleteManyArgs = { + /** + * Filter which ChatSessions to delete + */ + where?: Prisma.ChatSessionWhereInput + /** + * Limit how many ChatSessions to delete. + */ + limit?: number +} + +/** + * ChatSession without action + */ +export type ChatSessionDefaultArgs = { + /** + * Select specific fields to fetch from the ChatSession + */ + select?: Prisma.ChatSessionSelect | null + /** + * Omit specific fields from the ChatSession + */ + omit?: Prisma.ChatSessionOmit | null +} diff --git a/references/ai-chat/lib/generated/prisma/models/User.ts b/references/ai-chat/lib/generated/prisma/models/User.ts new file mode 100644 index 00000000000..1d1a6319da3 --- /dev/null +++ b/references/ai-chat/lib/generated/prisma/models/User.ts @@ -0,0 +1,1484 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `User` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums" +import type * as Prisma from "../internal/prismaNamespace" + +/** + * Model User + * + */ +export type UserModel = runtime.Types.Result.DefaultSelection + +export type AggregateUser = { + _count: UserCountAggregateOutputType | null + _avg: UserAvgAggregateOutputType | null + _sum: UserSumAggregateOutputType | null + _min: UserMinAggregateOutputType | null + _max: UserMaxAggregateOutputType | null +} + +export type UserAvgAggregateOutputType = { + messageCount: number | null +} + +export type UserSumAggregateOutputType = { + messageCount: number | null +} + +export type UserMinAggregateOutputType = { + id: string | null + name: string | null + plan: string | null + preferredModel: string | null + githubToken: string | null + messageCount: number | null + createdAt: Date | null + updatedAt: Date | null +} + +export type UserMaxAggregateOutputType = { + id: string | null + name: string | null + plan: string | null + preferredModel: string | null + githubToken: string | null + messageCount: number | null + createdAt: Date | null + updatedAt: Date | null +} + +export type UserCountAggregateOutputType = { + id: number + name: number + plan: number + preferredModel: number + githubToken: number + messageCount: number + createdAt: number + updatedAt: number + _all: number +} + + +export type UserAvgAggregateInputType = { + messageCount?: true +} + +export type UserSumAggregateInputType = { + messageCount?: true +} + +export type UserMinAggregateInputType = { + id?: true + name?: true + plan?: true + preferredModel?: true + githubToken?: true + messageCount?: true + createdAt?: true + updatedAt?: true +} + +export type UserMaxAggregateInputType = { + id?: true + name?: true + plan?: true + preferredModel?: true + githubToken?: true + messageCount?: true + createdAt?: true + updatedAt?: true +} + +export type UserCountAggregateInputType = { + id?: true + name?: true + plan?: true + preferredModel?: true + githubToken?: true + messageCount?: true + createdAt?: true + updatedAt?: true + _all?: true +} + +export type UserAggregateArgs = { + /** + * Filter which User to aggregate. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Users + **/ + _count?: true | UserCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to average + **/ + _avg?: UserAvgAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to sum + **/ + _sum?: UserSumAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: UserMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: UserMaxAggregateInputType +} + +export type GetUserAggregateType = { + [P in keyof T & keyof AggregateUser]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type UserGroupByArgs = { + where?: Prisma.UserWhereInput + orderBy?: Prisma.UserOrderByWithAggregationInput | Prisma.UserOrderByWithAggregationInput[] + by: Prisma.UserScalarFieldEnum[] | Prisma.UserScalarFieldEnum + having?: Prisma.UserScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: UserCountAggregateInputType | true + _avg?: UserAvgAggregateInputType + _sum?: UserSumAggregateInputType + _min?: UserMinAggregateInputType + _max?: UserMaxAggregateInputType +} + +export type UserGroupByOutputType = { + id: string + name: string + plan: string + preferredModel: string | null + githubToken: string | null + messageCount: number + createdAt: Date + updatedAt: Date + _count: UserCountAggregateOutputType | null + _avg: UserAvgAggregateOutputType | null + _sum: UserSumAggregateOutputType | null + _min: UserMinAggregateOutputType | null + _max: UserMaxAggregateOutputType | null +} + +type GetUserGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof UserGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type UserWhereInput = { + AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + OR?: Prisma.UserWhereInput[] + NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + id?: Prisma.StringFilter<"User"> | string + name?: Prisma.StringFilter<"User"> | string + plan?: Prisma.StringFilter<"User"> | string + preferredModel?: Prisma.StringNullableFilter<"User"> | string | null + githubToken?: Prisma.StringNullableFilter<"User"> | string | null + messageCount?: Prisma.IntFilter<"User"> | number + createdAt?: Prisma.DateTimeFilter<"User"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string + chats?: Prisma.ChatListRelationFilter +} + +export type UserOrderByWithRelationInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + plan?: Prisma.SortOrder + preferredModel?: Prisma.SortOrderInput | Prisma.SortOrder + githubToken?: Prisma.SortOrderInput | Prisma.SortOrder + messageCount?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder + chats?: Prisma.ChatOrderByRelationAggregateInput +} + +export type UserWhereUniqueInput = Prisma.AtLeast<{ + id?: string + AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + OR?: Prisma.UserWhereInput[] + NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + name?: Prisma.StringFilter<"User"> | string + plan?: Prisma.StringFilter<"User"> | string + preferredModel?: Prisma.StringNullableFilter<"User"> | string | null + githubToken?: Prisma.StringNullableFilter<"User"> | string | null + messageCount?: Prisma.IntFilter<"User"> | number + createdAt?: Prisma.DateTimeFilter<"User"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string + chats?: Prisma.ChatListRelationFilter +}, "id"> + +export type UserOrderByWithAggregationInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + plan?: Prisma.SortOrder + preferredModel?: Prisma.SortOrderInput | Prisma.SortOrder + githubToken?: Prisma.SortOrderInput | Prisma.SortOrder + messageCount?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder + _count?: Prisma.UserCountOrderByAggregateInput + _avg?: Prisma.UserAvgOrderByAggregateInput + _max?: Prisma.UserMaxOrderByAggregateInput + _min?: Prisma.UserMinOrderByAggregateInput + _sum?: Prisma.UserSumOrderByAggregateInput +} + +export type UserScalarWhereWithAggregatesInput = { + AND?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[] + OR?: Prisma.UserScalarWhereWithAggregatesInput[] + NOT?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[] + id?: Prisma.StringWithAggregatesFilter<"User"> | string + name?: Prisma.StringWithAggregatesFilter<"User"> | string + plan?: Prisma.StringWithAggregatesFilter<"User"> | string + preferredModel?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null + githubToken?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null + messageCount?: Prisma.IntWithAggregatesFilter<"User"> | number + createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string + updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string +} + +export type UserCreateInput = { + id: string + name: string + plan?: string + preferredModel?: string | null + githubToken?: string | null + messageCount?: number + createdAt?: Date | string + updatedAt?: Date | string + chats?: Prisma.ChatCreateNestedManyWithoutUserInput +} + +export type UserUncheckedCreateInput = { + id: string + name: string + plan?: string + preferredModel?: string | null + githubToken?: string | null + messageCount?: number + createdAt?: Date | string + updatedAt?: Date | string + chats?: Prisma.ChatUncheckedCreateNestedManyWithoutUserInput +} + +export type UserUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + plan?: Prisma.StringFieldUpdateOperationsInput | string + preferredModel?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + githubToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + messageCount?: Prisma.IntFieldUpdateOperationsInput | number + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + chats?: Prisma.ChatUpdateManyWithoutUserNestedInput +} + +export type UserUncheckedUpdateInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + plan?: Prisma.StringFieldUpdateOperationsInput | string + preferredModel?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + githubToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + messageCount?: Prisma.IntFieldUpdateOperationsInput | number + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + chats?: Prisma.ChatUncheckedUpdateManyWithoutUserNestedInput +} + +export type UserCreateManyInput = { + id: string + name: string + plan?: string + preferredModel?: string | null + githubToken?: string | null + messageCount?: number + createdAt?: Date | string + updatedAt?: Date | string +} + +export type UserUpdateManyMutationInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + plan?: Prisma.StringFieldUpdateOperationsInput | string + preferredModel?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + githubToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + messageCount?: Prisma.IntFieldUpdateOperationsInput | number + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type UserUncheckedUpdateManyInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + plan?: Prisma.StringFieldUpdateOperationsInput | string + preferredModel?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + githubToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + messageCount?: Prisma.IntFieldUpdateOperationsInput | number + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type UserCountOrderByAggregateInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + plan?: Prisma.SortOrder + preferredModel?: Prisma.SortOrder + githubToken?: Prisma.SortOrder + messageCount?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type UserAvgOrderByAggregateInput = { + messageCount?: Prisma.SortOrder +} + +export type UserMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + plan?: Prisma.SortOrder + preferredModel?: Prisma.SortOrder + githubToken?: Prisma.SortOrder + messageCount?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type UserMinOrderByAggregateInput = { + id?: Prisma.SortOrder + name?: Prisma.SortOrder + plan?: Prisma.SortOrder + preferredModel?: Prisma.SortOrder + githubToken?: Prisma.SortOrder + messageCount?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type UserSumOrderByAggregateInput = { + messageCount?: Prisma.SortOrder +} + +export type UserNullableScalarRelationFilter = { + is?: Prisma.UserWhereInput | null + isNot?: Prisma.UserWhereInput | null +} + +export type StringFieldUpdateOperationsInput = { + set?: string +} + +export type NullableStringFieldUpdateOperationsInput = { + set?: string | null +} + +export type IntFieldUpdateOperationsInput = { + set?: number + increment?: number + decrement?: number + multiply?: number + divide?: number +} + +export type DateTimeFieldUpdateOperationsInput = { + set?: Date | string +} + +export type UserCreateNestedOneWithoutChatsInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.UserCreateOrConnectWithoutChatsInput + connect?: Prisma.UserWhereUniqueInput +} + +export type UserUpdateOneWithoutChatsNestedInput = { + create?: Prisma.XOR + connectOrCreate?: Prisma.UserCreateOrConnectWithoutChatsInput + upsert?: Prisma.UserUpsertWithoutChatsInput + disconnect?: Prisma.UserWhereInput | boolean + delete?: Prisma.UserWhereInput | boolean + connect?: Prisma.UserWhereUniqueInput + update?: Prisma.XOR, Prisma.UserUncheckedUpdateWithoutChatsInput> +} + +export type UserCreateWithoutChatsInput = { + id: string + name: string + plan?: string + preferredModel?: string | null + githubToken?: string | null + messageCount?: number + createdAt?: Date | string + updatedAt?: Date | string +} + +export type UserUncheckedCreateWithoutChatsInput = { + id: string + name: string + plan?: string + preferredModel?: string | null + githubToken?: string | null + messageCount?: number + createdAt?: Date | string + updatedAt?: Date | string +} + +export type UserCreateOrConnectWithoutChatsInput = { + where: Prisma.UserWhereUniqueInput + create: Prisma.XOR +} + +export type UserUpsertWithoutChatsInput = { + update: Prisma.XOR + create: Prisma.XOR + where?: Prisma.UserWhereInput +} + +export type UserUpdateToOneWithWhereWithoutChatsInput = { + where?: Prisma.UserWhereInput + data: Prisma.XOR +} + +export type UserUpdateWithoutChatsInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + plan?: Prisma.StringFieldUpdateOperationsInput | string + preferredModel?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + githubToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + messageCount?: Prisma.IntFieldUpdateOperationsInput | number + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type UserUncheckedUpdateWithoutChatsInput = { + id?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.StringFieldUpdateOperationsInput | string + plan?: Prisma.StringFieldUpdateOperationsInput | string + preferredModel?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + githubToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + messageCount?: Prisma.IntFieldUpdateOperationsInput | number + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + + +/** + * Count Type UserCountOutputType + */ + +export type UserCountOutputType = { + chats: number +} + +export type UserCountOutputTypeSelect = { + chats?: boolean | UserCountOutputTypeCountChatsArgs +} + +/** + * UserCountOutputType without action + */ +export type UserCountOutputTypeDefaultArgs = { + /** + * Select specific fields to fetch from the UserCountOutputType + */ + select?: Prisma.UserCountOutputTypeSelect | null +} + +/** + * UserCountOutputType without action + */ +export type UserCountOutputTypeCountChatsArgs = { + where?: Prisma.ChatWhereInput +} + + +export type UserSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + name?: boolean + plan?: boolean + preferredModel?: boolean + githubToken?: boolean + messageCount?: boolean + createdAt?: boolean + updatedAt?: boolean + chats?: boolean | Prisma.User$chatsArgs + _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs +}, ExtArgs["result"]["user"]> + +export type UserSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + name?: boolean + plan?: boolean + preferredModel?: boolean + githubToken?: boolean + messageCount?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["user"]> + +export type UserSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + name?: boolean + plan?: boolean + preferredModel?: boolean + githubToken?: boolean + messageCount?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["user"]> + +export type UserSelectScalar = { + id?: boolean + name?: boolean + plan?: boolean + preferredModel?: boolean + githubToken?: boolean + messageCount?: boolean + createdAt?: boolean + updatedAt?: boolean +} + +export type UserOmit = runtime.Types.Extensions.GetOmit<"id" | "name" | "plan" | "preferredModel" | "githubToken" | "messageCount" | "createdAt" | "updatedAt", ExtArgs["result"]["user"]> +export type UserInclude = { + chats?: boolean | Prisma.User$chatsArgs + _count?: boolean | Prisma.UserCountOutputTypeDefaultArgs +} +export type UserIncludeCreateManyAndReturn = {} +export type UserIncludeUpdateManyAndReturn = {} + +export type $UserPayload = { + name: "User" + objects: { + chats: Prisma.$ChatPayload[] + } + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: string + name: string + plan: string + preferredModel: string | null + githubToken: string | null + messageCount: number + createdAt: Date + updatedAt: Date + }, ExtArgs["result"]["user"]> + composites: {} +} + +export type UserGetPayload = runtime.Types.Result.GetResult + +export type UserCountArgs = + Omit & { + select?: UserCountAggregateInputType | true + } + +export interface UserDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['User'], meta: { name: 'User' } } + /** + * Find zero or one User that matches the filter. + * @param {UserFindUniqueArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one User that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {UserFindUniqueOrThrowArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first User that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindFirstArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first User that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindFirstOrThrowArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more Users that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all Users + * const users = await prisma.user.findMany() + * + * // Get first 10 Users + * const users = await prisma.user.findMany({ take: 10 }) + * + * // Only select the `id` + * const userWithIdOnly = await prisma.user.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a User. + * @param {UserCreateArgs} args - Arguments to create a User. + * @example + * // Create one User + * const User = await prisma.user.create({ + * data: { + * // ... data to create a User + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many Users. + * @param {UserCreateManyArgs} args - Arguments to create many Users. + * @example + * // Create many Users + * const user = await prisma.user.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many Users and returns the data saved in the database. + * @param {UserCreateManyAndReturnArgs} args - Arguments to create many Users. + * @example + * // Create many Users + * const user = await prisma.user.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many Users and only return the `id` + * const userWithIdOnly = await prisma.user.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a User. + * @param {UserDeleteArgs} args - Arguments to delete one User. + * @example + * // Delete one User + * const User = await prisma.user.delete({ + * where: { + * // ... filter to delete one User + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one User. + * @param {UserUpdateArgs} args - Arguments to update one User. + * @example + * // Update one User + * const user = await prisma.user.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more Users. + * @param {UserDeleteManyArgs} args - Arguments to filter Users to delete. + * @example + * // Delete a few Users + * const { count } = await prisma.user.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Users. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Users + * const user = await prisma.user.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Users and returns the data updated in the database. + * @param {UserUpdateManyAndReturnArgs} args - Arguments to update many Users. + * @example + * // Update many Users + * const user = await prisma.user.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more Users and only return the `id` + * const userWithIdOnly = await prisma.user.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one User. + * @param {UserUpsertArgs} args - Arguments to update or create a User. + * @example + * // Update or create a User + * const user = await prisma.user.upsert({ + * create: { + * // ... data to create a User + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the User we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of Users. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserCountArgs} args - Arguments to filter Users to count. + * @example + * // Count the number of Users + * const count = await prisma.user.count({ + * where: { + * // ... the filter for the Users we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a User. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by User. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends UserGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: UserGroupByArgs['orderBy'] } + : { orderBy?: UserGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetUserGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the User model + */ +readonly fields: UserFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for User. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__UserClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + chats = {}>(args?: Prisma.Subset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions> | Null> + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the User model + */ +export interface UserFieldRefs { + readonly id: Prisma.FieldRef<"User", 'String'> + readonly name: Prisma.FieldRef<"User", 'String'> + readonly plan: Prisma.FieldRef<"User", 'String'> + readonly preferredModel: Prisma.FieldRef<"User", 'String'> + readonly githubToken: Prisma.FieldRef<"User", 'String'> + readonly messageCount: Prisma.FieldRef<"User", 'Int'> + readonly createdAt: Prisma.FieldRef<"User", 'DateTime'> + readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'> +} + + +// Custom InputTypes +/** + * User findUnique + */ +export type UserFindUniqueArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * Filter, which User to fetch. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User findUniqueOrThrow + */ +export type UserFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * Filter, which User to fetch. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User findFirst + */ +export type UserFindFirstArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * Filter, which User to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Users. + */ + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User findFirstOrThrow + */ +export type UserFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * Filter, which User to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Users. + */ + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User findMany + */ +export type UserFindManyArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * Filter, which Users to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User create + */ +export type UserCreateArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * The data needed to create a User. + */ + data: Prisma.XOR +} + +/** + * User createMany + */ +export type UserCreateManyArgs = { + /** + * The data used to create many Users. + */ + data: Prisma.UserCreateManyInput | Prisma.UserCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * User createManyAndReturn + */ +export type UserCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelectCreateManyAndReturn | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data used to create many Users. + */ + data: Prisma.UserCreateManyInput | Prisma.UserCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * User update + */ +export type UserUpdateArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * The data needed to update a User. + */ + data: Prisma.XOR + /** + * Choose, which User to update. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User updateMany + */ +export type UserUpdateManyArgs = { + /** + * The data used to update Users. + */ + data: Prisma.XOR + /** + * Filter which Users to update + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to update. + */ + limit?: number +} + +/** + * User updateManyAndReturn + */ +export type UserUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data used to update Users. + */ + data: Prisma.XOR + /** + * Filter which Users to update + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to update. + */ + limit?: number +} + +/** + * User upsert + */ +export type UserUpsertArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * The filter to search for the User to update in case it exists. + */ + where: Prisma.UserWhereUniqueInput + /** + * In case the User found by the `where` argument doesn't exist, create a new User with this data. + */ + create: Prisma.XOR + /** + * In case the User was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * User delete + */ +export type UserDeleteArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null + /** + * Filter which User to delete. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User deleteMany + */ +export type UserDeleteManyArgs = { + /** + * Filter which Users to delete + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to delete. + */ + limit?: number +} + +/** + * User.chats + */ +export type User$chatsArgs = { + /** + * Select specific fields to fetch from the Chat + */ + select?: Prisma.ChatSelect | null + /** + * Omit specific fields from the Chat + */ + omit?: Prisma.ChatOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.ChatInclude | null + where?: Prisma.ChatWhereInput + orderBy?: Prisma.ChatOrderByWithRelationInput | Prisma.ChatOrderByWithRelationInput[] + cursor?: Prisma.ChatWhereUniqueInput + take?: number + skip?: number + distinct?: Prisma.ChatScalarFieldEnum | Prisma.ChatScalarFieldEnum[] +} + +/** + * User without action + */ +export type UserDefaultArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Choose, which related nodes to fetch as well + */ + include?: Prisma.UserInclude | null +} diff --git a/references/ai-chat/next-env.d.ts b/references/ai-chat/next-env.d.ts new file mode 100644 index 00000000000..1b3be0840f3 --- /dev/null +++ b/references/ai-chat/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/references/ai-chat/next.config.ts b/references/ai-chat/next.config.ts new file mode 100644 index 00000000000..ca6c9392a18 --- /dev/null +++ b/references/ai-chat/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + devIndicators: false, +}; + +export default nextConfig; diff --git a/references/ai-chat/package.json b/references/ai-chat/package.json new file mode 100644 index 00000000000..45071838a68 --- /dev/null +++ b/references/ai-chat/package.json @@ -0,0 +1,48 @@ +{ + "name": "references-ai-chat", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "dev:trigger": "trigger dev", + "db:migrate": "prisma migrate dev", + "db:push": "prisma db push", + "db:generate": "prisma generate", + "db:reset:chats": "prisma db execute --file prisma/reset-chats.sql", + "test": "vitest" + }, + "dependencies": { + "@ai-sdk/anthropic": "^3.0.0", + "@ai-sdk/openai": "^3.0.0", + "@ai-sdk/react": "^3.0.0", + "@prisma/adapter-pg": "^7.4.2", + "@prisma/client": "^7.4.2", + "@e2b/code-interpreter": "^2.4.0", + "@trigger.dev/sdk": "workspace:*", + "serialize-error": "^11.0.3", + "ai": "^6.0.0", + "next": "15.3.3", + "pg": "^8.16.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "streamdown": "^2.3.0", + "turndown": "^7.2.2", + "zod": "3.25.76" + }, + "devDependencies": { + "@ai-sdk/provider": "3.0.8", + "@tailwindcss/postcss": "^4", + "@trigger.dev/build": "workspace:*", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/turndown": "^5.0.6", + "prisma": "^7.4.2", + "tailwindcss": "^4", + "trigger.dev": "workspace:*", + "typescript": "^5", + "vitest": "^3.1.4" + } +} diff --git a/references/ai-chat/postcss.config.mjs b/references/ai-chat/postcss.config.mjs new file mode 100644 index 00000000000..79bcf135dc4 --- /dev/null +++ b/references/ai-chat/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/references/ai-chat/prisma.config.ts b/references/ai-chat/prisma.config.ts new file mode 100644 index 00000000000..d73df7b3168 --- /dev/null +++ b/references/ai-chat/prisma.config.ts @@ -0,0 +1,12 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/references/ai-chat/prisma/migrations/20260305112427_init/migration.sql b/references/ai-chat/prisma/migrations/20260305112427_init/migration.sql new file mode 100644 index 00000000000..951cd33d94e --- /dev/null +++ b/references/ai-chat/prisma/migrations/20260305112427_init/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "Chat" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "messages" JSONB NOT NULL DEFAULT '[]', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Chat_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ChatSession" ( + "id" TEXT NOT NULL, + "runId" TEXT NOT NULL, + "publicAccessToken" TEXT NOT NULL, + "lastEventId" TEXT, + + CONSTRAINT "ChatSession_pkey" PRIMARY KEY ("id") +); diff --git a/references/ai-chat/prisma/migrations/20260306165319_add_user_model/migration.sql b/references/ai-chat/prisma/migrations/20260306165319_add_user_model/migration.sql new file mode 100644 index 00000000000..4a1bca35872 --- /dev/null +++ b/references/ai-chat/prisma/migrations/20260306165319_add_user_model/migration.sql @@ -0,0 +1,18 @@ +-- AlterTable +ALTER TABLE "Chat" ADD COLUMN "userId" TEXT; + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "plan" TEXT NOT NULL DEFAULT 'free', + "preferredModel" TEXT, + "messageCount" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/references/ai-chat/prisma/migrations/20260327180000_remove_user_tool/migration.sql b/references/ai-chat/prisma/migrations/20260327180000_remove_user_tool/migration.sql new file mode 100644 index 00000000000..c7a35afc8f2 --- /dev/null +++ b/references/ai-chat/prisma/migrations/20260327180000_remove_user_tool/migration.sql @@ -0,0 +1,2 @@ +-- DropTable +DROP TABLE IF EXISTS "UserTool"; diff --git a/references/ai-chat/prisma/migrations/20260425091008_add_chat_model_and_user_github_token/migration.sql b/references/ai-chat/prisma/migrations/20260425091008_add_chat_model_and_user_github_token/migration.sql new file mode 100644 index 00000000000..277ee3276f8 --- /dev/null +++ b/references/ai-chat/prisma/migrations/20260425091008_add_chat_model_and_user_github_token/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Chat" ADD COLUMN "model" TEXT NOT NULL DEFAULT 'gpt-4o-mini'; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "githubToken" TEXT; diff --git a/references/ai-chat/prisma/migrations/20260425121916_add_session_id_to_chat_session/migration.sql b/references/ai-chat/prisma/migrations/20260425121916_add_session_id_to_chat_session/migration.sql new file mode 100644 index 00000000000..ee079856702 --- /dev/null +++ b/references/ai-chat/prisma/migrations/20260425121916_add_session_id_to_chat_session/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ChatSession" ADD COLUMN "sessionId" TEXT; diff --git a/references/ai-chat/prisma/migrations/20260427053743_simplify_chat_session_for_run_manager/migration.sql b/references/ai-chat/prisma/migrations/20260427053743_simplify_chat_session_for_run_manager/migration.sql new file mode 100644 index 00000000000..2b5d51564df --- /dev/null +++ b/references/ai-chat/prisma/migrations/20260427053743_simplify_chat_session_for_run_manager/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `runId` on the `ChatSession` table. All the data in the column will be lost. + - You are about to drop the column `sessionId` on the `ChatSession` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "ChatSession" DROP COLUMN "runId", +DROP COLUMN "sessionId"; diff --git a/references/ai-chat/prisma/migrations/migration_lock.toml b/references/ai-chat/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000000..044d57cdb0d --- /dev/null +++ b/references/ai-chat/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/references/ai-chat/prisma/reset-chats.sql b/references/ai-chat/prisma/reset-chats.sql new file mode 100644 index 00000000000..06363e73397 --- /dev/null +++ b/references/ai-chat/prisma/reset-chats.sql @@ -0,0 +1,7 @@ +-- Wipe customer-side chat state for a fresh smoke-test slate. +-- Run via `pnpm run db:reset:chats`. +-- Leaves User rows intact (they're upserted by onPreload/onChatStart), +-- but clears every Chat + ChatSession so a chatId from one target +-- (test cloud / local) can't carry stale session/PAT/lastEventId state +-- into the other. +TRUNCATE "Chat", "ChatSession"; diff --git a/references/ai-chat/prisma/schema.prisma b/references/ai-chat/prisma/schema.prisma new file mode 100644 index 00000000000..51d6437a9b3 --- /dev/null +++ b/references/ai-chat/prisma/schema.prisma @@ -0,0 +1,42 @@ +generator client { + provider = "prisma-client" + output = "../lib/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +model User { + id String @id + name String + plan String @default("free") // "free" | "pro" + preferredModel String? + githubToken String? + messageCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + chats Chat[] +} + +model Chat { + id String @id + title String + model String @default("gpt-4o-mini") + messages Json @default("[]") + userId String? + user User? @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Persistable session state for a chat. After the Sessions-as-run-manager +// refactor, the transport addresses by `chatId` (used as the Session +// `externalId`) on every wire path — so we only need a session-scoped +// PAT and the SSE last-event-id for resume. Runs come and go inside +// the Session and are managed server-side. +model ChatSession { + id String @id // chatId + publicAccessToken String + lastEventId String? +} diff --git a/references/ai-chat/src/app/actions.ts b/references/ai-chat/src/app/actions.ts new file mode 100644 index 00000000000..0ef650cfc8c --- /dev/null +++ b/references/ai-chat/src/app/actions.ts @@ -0,0 +1,194 @@ +"use server"; + +import { auth } from "@trigger.dev/sdk"; +import { chat } from "@trigger.dev/sdk/ai"; +import type { + aiChat, + aiChatHydrated, + aiChatRaw, + aiChatSession, + upgradeTestAgent, +} from "@/trigger/chat"; +import type { ChatUiMessage } from "@/lib/chat-tools-schemas"; +import { prisma } from "@/lib/prisma"; + +/** Short-lived PATs for local testing of expiry + renewal (not for production). */ +const CHAT_EXAMPLE_PAT_TTL = "1h" as const; + +export type ChatReferenceTaskId = + | "ai-chat" + | "ai-chat-hydrated" + | "ai-chat-raw" + | "ai-chat-session" + | "upgrade-test"; + +function isChatReferenceTaskId(id: string): id is ChatReferenceTaskId { + return ( + id === "ai-chat" || + id === "ai-chat-hydrated" || + id === "ai-chat-raw" || + id === "ai-chat-session" || + id === "upgrade-test" + ); +} + +/** Keeps compile-time alignment with exported chat tasks. */ +type TaskIdentifierForChat = + | (typeof aiChat)["id"] + | (typeof aiChatHydrated)["id"] + | (typeof aiChatRaw)["id"] + | (typeof aiChatSession)["id"] + | (typeof upgradeTestAgent)["id"]; + +/** + * Server-mediated start: creates the Session row + triggers the first + * run via secret-key access, returns the session-scoped PAT for the + * browser to use. Wired into the transport's `startSession` callback — + * the transport invokes it on `transport.preload(chatId)` and lazily on + * the first `sendMessage` for any chatId without a cached PAT. + * + * The browser never sees a `start` token in this path; the customer's + * server keeps the secret. + * + * `clientData` flows through from the transport's typed `clientData` + * option — same value the transport merges into per-turn `metadata` + * — and lands in `triggerConfig.basePayload.metadata` so the first + * run's `payload.metadata` (visible to `onPreload` / `onChatStart`) + * matches what subsequent turns see. Server-side authorization can + * still override or augment what the browser claims (e.g. ignore a + * spoofed userId and substitute the request-session's userId). + */ +const startChatSessionFor = (taskId: TaskIdentifierForChat) => + chat.createStartSessionAction(taskId, { tokenTTL: CHAT_EXAMPLE_PAT_TTL }); + +const startActionByTaskId: Record< + ChatReferenceTaskId, + ReturnType +> = { + "ai-chat": startChatSessionFor("ai-chat"), + "ai-chat-hydrated": startChatSessionFor("ai-chat-hydrated"), + "ai-chat-raw": startChatSessionFor("ai-chat-raw"), + "ai-chat-session": startChatSessionFor("ai-chat-session"), + "upgrade-test": startChatSessionFor("upgrade-test"), +}; + +export async function startChatSession(input: { + chatId: string; + taskId?: string; + clientData?: Record; +}): Promise<{ publicAccessToken: string }> { + const id = input.taskId ?? "ai-chat"; + const taskId: ChatReferenceTaskId = !isChatReferenceTaskId(id) ? "ai-chat" : id; + + // `clientData` arrives from the transport's typed `clientData` option. + // In a real app the server would also resolve the user from the + // request session and merge/override accordingly — never trust the + // browser-claimed identity. The reference demo just trusts it. + const result = await startActionByTaskId[taskId]({ + chatId: input.chatId, + triggerConfig: input.clientData + ? { basePayload: { metadata: input.clientData } } + : undefined, + }); + + // Persist the latest PAT alongside the chat so a fresh tab can + // hydrate without going through the start callback again. + await prisma.chatSession + .upsert({ + where: { id: input.chatId }, + create: { id: input.chatId, publicAccessToken: result.publicAccessToken }, + update: { publicAccessToken: result.publicAccessToken }, + }) + .catch(() => { + /* best-effort persistence */ + }); + + return { publicAccessToken: result.publicAccessToken }; +} + +/** + * Mint a session-scoped PAT for a chatId. Pure: just calls + * `auth.createPublicToken` with `read:sessions:{chatId}` + + * `write:sessions:{chatId}` scopes — no DB writes, no session + * creation, no run triggering. + * + * The transport's `accessToken` callback wraps this. It fires on + * initial use (when no PAT is hydrated) and on 401/403 refresh. + * Session creation happens separately via `startChatSession` at page + * load — keeping these concerns split avoids re-triggering runs every + * time a PAT expires. + */ +export async function mintChatAccessToken(chatId: string): Promise { + return auth.createPublicToken({ + scopes: { + read: { sessions: chatId }, + write: { sessions: chatId }, + }, + expirationTime: CHAT_EXAMPLE_PAT_TTL, + }); +} + +export async function getChatList() { + const chats = await prisma.chat.findMany({ + select: { id: true, title: true, model: true, createdAt: true, updatedAt: true }, + orderBy: { updatedAt: "desc" }, + }); + return chats.map((c) => ({ + id: c.id, + title: c.title, + model: c.model, + createdAt: c.createdAt.getTime(), + updatedAt: c.updatedAt.getTime(), + })); +} + +export async function getChatMessages(chatId: string): Promise { + const found = await prisma.chat.findUnique({ where: { id: chatId } }); + if (!found) return []; + return found.messages as unknown as ChatUiMessage[]; +} + +export async function deleteChat(chatId: string) { + await prisma.chat.delete({ where: { id: chatId } }).catch(() => { }); + await prisma.chatSession.delete({ where: { id: chatId } }).catch(() => { }); +} + +export async function deleteAllChats() { + await prisma.chatSession.deleteMany(); + await prisma.chat.deleteMany(); +} + +export async function updateChatTitle(chatId: string, title: string) { + await prisma.chat.update({ where: { id: chatId }, data: { title } }).catch(() => { }); +} + +export async function updateSessionLastEventId(chatId: string, lastEventId: string) { + await prisma.chatSession + .update({ where: { id: chatId }, data: { lastEventId } }) + .catch(() => { }); +} + +export async function deleteSessionAction(chatId: string) { + await prisma.chatSession.delete({ where: { id: chatId } }).catch(() => { }); +} + +export async function getSessionForChat(chatId: string) { + const session = await prisma.chatSession.findUnique({ where: { id: chatId } }); + if (!session) return null; + return { + publicAccessToken: session.publicAccessToken, + lastEventId: session.lastEventId ?? undefined, + }; +} + +export async function getAllSessions() { + const sessions = await prisma.chatSession.findMany(); + const result: Record = {}; + for (const s of sessions) { + result[s.id] = { + publicAccessToken: s.publicAccessToken, + lastEventId: s.lastEventId ?? undefined, + }; + } + return result; +} diff --git a/references/ai-chat/src/app/api/chat/route.ts b/references/ai-chat/src/app/api/chat/route.ts new file mode 100644 index 00000000000..42812f16399 --- /dev/null +++ b/references/ai-chat/src/app/api/chat/route.ts @@ -0,0 +1,54 @@ +/** + * chat.headStart first-turn endpoint. + * + * The browser transport POSTs first-turn messages here when the + * `headStart` option is set on `useTriggerChatTransport`. This + * handler: + * + * 1. Creates the chat.agent session and triggers a `handover-prepare` + * run (atomic, one round-trip), so the agent boots in parallel. + * 2. Runs `streamText` step 1 right here in the warm Next.js process + * and returns the SSE stream directly to the browser — no waiting + * on the agent's cold start. + * 3. On step 1's tool-call boundary, hands ownership of the durable + * session.out stream over to the agent run, which executes tools + * and continues from step 2+ (or exits clean for pure-text turns). + * + * Subsequent turns bypass this endpoint — the transport hydrates the + * session PAT from response headers and writes directly to + * `session.in` for turn 2 onward. + * + * The TTFC win: cold-start agent boot (~488ms) + onTurnStart hooks + * (~316ms) overlap with the LLM TTFB instead of stacking before it, + * so the user-perceived first chunk arrives ~50% sooner. The agent + * still owns tool execution and everything after — heavy deps stay + * where they belong. + */ +import { chat } from "@trigger.dev/sdk/chat-server"; +import { streamText } from "ai"; +import { anthropic } from "@ai-sdk/anthropic"; +// ⚠️ Imports MUST come from `chat-tools-schemas` only — see the +// header comment in that file for the bundle-isolation rationale. +// Importing `src/trigger/chat-tools.ts` here would drag E2B, +// turndown, the trigger SDK runtime, etc. into the Next.js bundle +// and defeat the whole point of `chat.headStart`. +import { headStartTools } from "@/lib/chat-tools-schemas"; + +export const POST = chat.headStart({ + agentId: "ai-chat", + run: async ({ chat: chatHelper }) => { + return streamText({ + // `toStreamTextOptions` wires `messages` (converted from + // UIMessages), `tools`, `stopWhen: stepCountIs(1)`, and the + // combined `abortSignal`. Customer adds model + system prompt on + // top — anything else `streamText` accepts is fair game. + ...chatHelper.toStreamTextOptions({ tools: headStartTools }), + // Match the agent's default (`DEFAULT_MODEL` in `lib/models.ts`) + // so step 1 and step 2+ run on the same provider — no jarring + // tone/style shift mid-turn, and TTFC comparisons stay honest. + model: anthropic("claude-sonnet-4-6"), + system: + "You are a helpful AI assistant. Be concise and friendly. Use the available tools when relevant.", + }); + }, +}); diff --git a/references/ai-chat/src/app/chats/[chatId]/page.tsx b/references/ai-chat/src/app/chats/[chatId]/page.tsx new file mode 100644 index 00000000000..a75d22e518e --- /dev/null +++ b/references/ai-chat/src/app/chats/[chatId]/page.tsx @@ -0,0 +1,40 @@ +import { + getChatMessages, + getSessionForChat, + getChatList, +} from "@/app/actions"; +import { ChatView } from "@/components/chat-view"; +import { DEFAULT_MODEL } from "@/lib/models"; + +export default async function ChatPage({ + params, +}: { + params: Promise<{ chatId: string }>; +}) { + const { chatId } = await params; + + // Hydrate any persisted session PAT from a previous visit. For brand + // new chats `getSessionForChat` returns null and the client-side + // `chat-view.tsx` mount triggers `startChatSession` with the + // user-selected `taskMode` — the server-rendered page can't see the + // dropdown's React-context state. + const [messages, session, chatList] = await Promise.all([ + getChatMessages(chatId), + getSessionForChat(chatId), + getChatList(), + ]); + + const chatMeta = chatList.find((c) => c.id === chatId); + const isNewChat = !chatMeta; + const model = chatMeta?.model ?? DEFAULT_MODEL; + + return ( + + ); +} diff --git a/references/ai-chat/src/app/chats/layout.tsx b/references/ai-chat/src/app/chats/layout.tsx new file mode 100644 index 00000000000..d76cd85f23d --- /dev/null +++ b/references/ai-chat/src/app/chats/layout.tsx @@ -0,0 +1,16 @@ +import { getChatList } from "@/app/actions"; +import { ChatSettingsProvider } from "@/components/chat-settings-context"; +import { ChatSidebarWrapper } from "@/components/chat-sidebar-wrapper"; + +export default async function ChatsLayout({ children }: { children: React.ReactNode }) { + const chatList = await getChatList(); + + return ( + +

+ +
{children}
+
+ + ); +} diff --git a/references/ai-chat/src/app/chats/page.tsx b/references/ai-chat/src/app/chats/page.tsx new file mode 100644 index 00000000000..04cd57b7012 --- /dev/null +++ b/references/ai-chat/src/app/chats/page.tsx @@ -0,0 +1,16 @@ +import { getChatList } from "@/app/actions"; +import { redirect } from "next/navigation"; + +export default async function ChatsPage() { + const chatList = await getChatList(); + + if (chatList.length > 0) { + redirect(`/chats/${chatList[0]!.id}`); + } + + return ( +
+

No conversations yet. Start a new chat.

+
+ ); +} diff --git a/references/ai-chat/src/app/globals.css b/references/ai-chat/src/app/globals.css new file mode 100644 index 00000000000..92c4b9a7860 --- /dev/null +++ b/references/ai-chat/src/app/globals.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; +@source "../../../node_modules/streamdown/dist/*.js"; diff --git a/references/ai-chat/src/app/layout.tsx b/references/ai-chat/src/app/layout.tsx new file mode 100644 index 00000000000..544dd9142d8 --- /dev/null +++ b/references/ai-chat/src/app/layout.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import "streamdown/styles.css"; + +export const metadata: Metadata = { + title: "AI Chat — Trigger.dev", + description: "AI SDK useChat powered by Trigger.dev durable tasks", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/references/ai-chat/src/app/page.tsx b/references/ai-chat/src/app/page.tsx new file mode 100644 index 00000000000..6792870396c --- /dev/null +++ b/references/ai-chat/src/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Home() { + redirect("/chats"); +} diff --git a/references/ai-chat/src/components/chat-app.tsx b/references/ai-chat/src/components/chat-app.tsx new file mode 100644 index 00000000000..4d9ede23eba --- /dev/null +++ b/references/ai-chat/src/components/chat-app.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { generateId } from "ai"; +import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; +import type { ChatUiMessage } from "@/lib/chat-tools-schemas"; +import { useCallback, useEffect, useState } from "react"; +import { Chat } from "@/components/chat"; +import { ChatSidebar } from "@/components/chat-sidebar"; +import { DEFAULT_MODEL } from "@/lib/models"; +import { + mintChatAccessToken, + startChatSession, + getChatList, + getChatMessages, + deleteChat as deleteChatAction, + deleteAllChats, + updateChatTitle, + deleteSessionAction, +} from "@/app/actions"; + +type ChatMeta = { + id: string; + title: string; + model: string; + createdAt: number; + updatedAt: number; +}; + +type SessionInfo = { + publicAccessToken: string; + lastEventId?: string; +}; + +type ChatAppProps = { + taskMode: string; + onTaskModeChange: (mode: string) => void; + initialChatList: ChatMeta[]; + initialActiveChatId: string | null; + initialMessages: ChatUiMessage[]; + initialSessions: Record; +}; + +export function ChatApp({ + taskMode, + onTaskModeChange, + initialChatList, + initialActiveChatId, + initialMessages, + initialSessions, +}: ChatAppProps) { + const [chatList, setChatList] = useState(initialChatList); + const [activeChatId, setActiveChatId] = useState(initialActiveChatId); + const [messages, setMessages] = useState(initialMessages); + const [sessions, setSessions] = useState>(initialSessions); + + // Model for new chats (before first message is sent) + const [newChatModel, setNewChatModel] = useState(DEFAULT_MODEL); + const [idleTimeoutInSeconds, setIdleTimeoutInSeconds] = useState(60); + + const handleSessionChange = useCallback((chatId: string, session: SessionInfo | null) => { + if (session) { + setSessions((prev) => ({ ...prev, [chatId]: session })); + } else { + setSessions((prev) => { + const next = { ...prev }; + delete next[chatId]; + return next; + }); + deleteSessionAction(chatId); + } + }, []); + + const transport = useTriggerChatTransport({ + task: taskMode, + // Pure mint — server action calls `auth.createPublicToken({ scopes: + // { sessions: chatId } })`. Fired on 401/403 refresh. + accessToken: ({ chatId }) => mintChatAccessToken(chatId), + // Session create — server action wraps `chat.createStartSessionAction`. + // Transport invokes it on `preload(chatId)` and lazily on first + // `sendMessage` for any chatId without a cached PAT. `clientData` + // is threaded through to `triggerConfig.basePayload.metadata` so + // the first run sees the same shape as per-turn `metadata`. + startSession: ({ chatId, taskId, clientData }) => + startChatSession({ chatId, taskId, clientData }), + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + sessions: initialSessions, + onSessionChange: handleSessionChange, + clientData: { userId: "user_123" }, + }); + + // Load messages when active chat changes + useEffect(() => { + if (!activeChatId) { + setMessages([]); + return; + } + // Don't reload if we already have the initial messages for the initial chat + if (activeChatId === initialActiveChatId && messages === initialMessages) { + return; + } + getChatMessages(activeChatId).then(setMessages); + }, [activeChatId]); + + function handleNewChat() { + const id = generateId(); + setActiveChatId(id); + setMessages([]); + setNewChatModel(DEFAULT_MODEL); + void idleTimeoutInSeconds; + } + + function handleSelectChat(id: string) { + setActiveChatId(id); + } + + async function handleDeleteChat(id: string) { + await deleteChatAction(id); + const list = await getChatList(); + setChatList(list); + if (activeChatId === id) { + if (list.length > 0) { + setActiveChatId(list[0]!.id); + } else { + setActiveChatId(null); + } + } + } + + async function handleWipeAll() { + await deleteAllChats(); + setChatList([]); + setActiveChatId(null); + setMessages([]); + setSessions({}); + } + + const handleFirstMessage = useCallback(async (chatId: string, text: string) => { + const title = text.slice(0, 40).trim() || "New chat"; + await updateChatTitle(chatId, title); + const list = await getChatList(); + setChatList(list); + }, []); + + const handleMessagesChange = useCallback(async (_chatId: string, _messages: ChatUiMessage[]) => { + // Messages are persisted server-side via onTurnComplete. + // Refresh the chat list to update timestamps. + const list = await getChatList(); + setChatList(list); + }, []); + + // Determine the model for the active chat + const activeChatMeta = chatList.find((c) => c.id === activeChatId); + const isNewChat = activeChatId != null && !activeChatMeta; + const activeModel = isNewChat ? newChatModel : activeChatMeta?.model ?? DEFAULT_MODEL; + + // Get session for the active chat + const activeSession = activeChatId ? sessions[activeChatId] : undefined; + + return ( +
+ {}} + /> +
+ {activeChatId ? ( + 0} + model={activeModel} + isNewChat={isNewChat} + onModelChange={isNewChat ? setNewChatModel : undefined} + session={activeSession} + dashboardUrl={process.env.NEXT_PUBLIC_TRIGGER_DASHBOARD_URL} + projectDashboardPath={process.env.NEXT_PUBLIC_TRIGGER_PROJECT_DASHBOARD_PATH} + onFirstMessage={handleFirstMessage} + onMessagesChange={handleMessagesChange} + /> + ) : ( +
+
+

No conversation selected

+ +
+
+ )} +
+
+ ); +} diff --git a/references/ai-chat/src/components/chat-settings-context.tsx b/references/ai-chat/src/components/chat-settings-context.tsx new file mode 100644 index 00000000000..6eb6366ca01 --- /dev/null +++ b/references/ai-chat/src/components/chat-settings-context.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { createContext, useContext, useState, type ReactNode } from "react"; + +type ChatSettings = { + taskMode: string; + setTaskMode: (mode: string) => void; + idleTimeoutInSeconds: number; + setIdleTimeoutInSeconds: (seconds: number) => void; + /** + * When true, first-turn messages are POSTed to `/api/chat` + * (`chat.handover` route handler) instead of triggering the agent + * directly. Subsequent turns bypass the endpoint regardless. + */ + useHandover: boolean; + setUseHandover: (on: boolean) => void; +}; + +const ChatSettingsContext = createContext(null); + +export function ChatSettingsProvider({ children }: { children: ReactNode }) { + const [taskMode, setTaskMode] = useState("ai-chat"); + const [idleTimeoutInSeconds, setIdleTimeoutInSeconds] = useState(60); + const [useHandover, setUseHandover] = useState(false); + + const value: ChatSettings = { + taskMode, + setTaskMode, + idleTimeoutInSeconds, + setIdleTimeoutInSeconds, + useHandover, + setUseHandover, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Provider = ChatSettingsContext.Provider as any; + + return {children}; +} + +export function useChatSettings() { + const ctx = useContext(ChatSettingsContext); + if (!ctx) throw new Error("useChatSettings must be used within ChatSettingsProvider"); + return ctx; +} diff --git a/references/ai-chat/src/components/chat-sidebar-wrapper.tsx b/references/ai-chat/src/components/chat-sidebar-wrapper.tsx new file mode 100644 index 00000000000..8b3dc3b8e51 --- /dev/null +++ b/references/ai-chat/src/components/chat-sidebar-wrapper.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useRouter, usePathname } from "next/navigation"; +import { ChatSidebar } from "@/components/chat-sidebar"; +import { useChatSettings } from "@/components/chat-settings-context"; +import { useState, useCallback, useEffect } from "react"; +import { generateId } from "ai"; +import { getChatList, deleteChat as deleteChatAction, deleteAllChats } from "@/app/actions"; + +type ChatMeta = { + id: string; + title: string; + model: string; + createdAt: number; + updatedAt: number; +}; + +export function ChatSidebarWrapper({ + initialChatList, +}: { + initialChatList: ChatMeta[]; +}) { + const router = useRouter(); + const pathname = usePathname(); + const [chatList, setChatList] = useState(initialChatList); + const { + taskMode, + setTaskMode, + idleTimeoutInSeconds, + setIdleTimeoutInSeconds, + useHandover, + setUseHandover, + } = useChatSettings(); + + // Extract active chatId from URL + const activeChatId = + pathname?.startsWith("/chats/") ? (pathname.split("/chats/")[1]?.split("/")[0] ?? null) : null; + + const refreshChatList = useCallback(async () => { + const list = await getChatList(); + setChatList(list); + }, []); + + // Refresh chat list on navigation + useEffect(() => { + refreshChatList(); + }, [pathname, refreshChatList]); + + function handleSelectChat(id: string) { + router.push(`/chats/${id}`); + } + + function handleNewChat() { + const id = generateId(); + router.push(`/chats/${id}`); + } + + async function handleDeleteChat(id: string) { + await deleteChatAction(id); + const list = await getChatList(); + setChatList(list); + if (activeChatId === id) { + if (list.length > 0) { + router.push(`/chats/${list[0]!.id}`); + } else { + router.push("/chats"); + } + } + } + + async function handleWipeAll() { + if (!confirm("Delete ALL chats? This cannot be undone.")) return; + await deleteAllChats(); + setChatList([]); + router.push("/chats"); + } + + return ( + + ); +} diff --git a/references/ai-chat/src/components/chat-sidebar.tsx b/references/ai-chat/src/components/chat-sidebar.tsx new file mode 100644 index 00000000000..e036eebc71a --- /dev/null +++ b/references/ai-chat/src/components/chat-sidebar.tsx @@ -0,0 +1,145 @@ +"use client"; + +type ChatMeta = { + id: string; + title: string; + createdAt: number; + updatedAt: number; +}; + +function timeAgo(ts: number): string { + const seconds = Math.floor((Date.now() - ts) / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +type ChatSidebarProps = { + chats: ChatMeta[]; + activeChatId: string | null; + onSelectChat: (id: string) => void; + onNewChat: () => void; + onDeleteChat: (id: string) => void; + onWipeAll: () => void; + idleTimeoutInSeconds: number; + onIdleTimeoutChange: (seconds: number) => void; + taskMode: string; + onTaskModeChange: (mode: string) => void; + useHandover: boolean; + onUseHandoverChange: (on: boolean) => void; +}; + +export function ChatSidebar({ + chats, + activeChatId, + onSelectChat, + onNewChat, + onDeleteChat, + onWipeAll, + idleTimeoutInSeconds, + onIdleTimeoutChange, + taskMode, + onTaskModeChange, + useHandover, + onUseHandoverChange, +}: ChatSidebarProps) { + const sorted = [...chats].sort((a, b) => b.updatedAt - a.updatedAt); + + return ( +
+
+ +
+ +
+ {sorted.length === 0 && ( +

No conversations yet

+ )} + + {sorted.map((chat) => ( + + ))} +
+ +
+
+ Idle timeout + onIdleTimeoutChange(Number(e.target.value))} + className="w-16 rounded border border-gray-300 px-1.5 py-0.5 text-xs text-gray-600 outline-none focus:border-blue-500" + /> + s +
+
+ Task + +
+ + +
+
+ ); +} diff --git a/references/ai-chat/src/components/chat-view.tsx b/references/ai-chat/src/components/chat-view.tsx new file mode 100644 index 00000000000..5c1d52296ec --- /dev/null +++ b/references/ai-chat/src/components/chat-view.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; +import type { ChatUiMessage } from "@/lib/chat-tools-schemas"; +import { Chat } from "@/components/chat"; +import { useChatSettings } from "@/components/chat-settings-context"; +import { + mintChatAccessToken, + startChatSession, + updateChatTitle, + deleteSessionAction, +} from "@/app/actions"; +import { useCallback, useState } from "react"; +import { useRouter } from "next/navigation"; + +type SessionInfo = { + publicAccessToken: string; + lastEventId?: string; +}; + +type ChatViewProps = { + chatId: string; + initialMessages: ChatUiMessage[]; + initialSession: SessionInfo | null; + isNewChat: boolean; + model: string; +}; + +export function ChatView({ + chatId, + initialMessages, + initialSession, + isNewChat, + model, +}: ChatViewProps) { + const router = useRouter(); + const { taskMode, useHandover } = useChatSettings(); + + const [currentSession, setCurrentSession] = useState(initialSession); + + const handleSessionChange = useCallback((id: string, session: SessionInfo | null) => { + if (session) { + setCurrentSession(session); + } else { + setCurrentSession(null); + deleteSessionAction(id); + } + }, []); + + const transport = useTriggerChatTransport({ + task: taskMode, + // Pure mint — server action calls `auth.createPublicToken({ scopes: + // { sessions: chatId } })` and returns the JWT. Fired on 401/403 to + // refresh the session PAT. Never creates a session. + accessToken: ({ chatId }) => mintChatAccessToken(chatId), + // Session create — server action wraps `chat.createStartSessionAction` + // (secret-key auth, server-side authorization). Idempotent on + // `(env, externalId)`. Transport invokes it on `preload(chatId)` + // and lazily on first `sendMessage` for any chatId without a + // cached PAT. `clientData` is the transport's typed `clientData` + // option, threaded through so the first run's `payload.metadata` + // matches per-turn `metadata`. + startSession: ({ chatId, taskId, clientData }) => + startChatSession({ chatId, taskId, clientData }), + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + sessions: initialSession ? { [chatId]: initialSession } : {}, + onSessionChange: handleSessionChange, + clientData: { userId: "user_123" }, + multiTab: true, + // Head-start URL: opt-in fast-path for the first message of a + // brand-new chat. The transport POSTs to `/api/chat` (which + // exports `chat.handover({ agentId, run })`) so step 1's LLM + // call runs in the warm Next.js process while the trigger agent + // run boots in parallel. After turn 1 the transport hydrates + // session state from response headers and writes directly to + // `session.in` for turn 2 onward — same direct-trigger path as + // when `headStart` is unset. + headStart: useHandover ? "/api/chat" : undefined, + }); + + const handleFirstMessage = useCallback( + async (cId: string, text: string) => { + const title = text.slice(0, 40).trim() || "New chat"; + await updateChatTitle(cId, title); + router.refresh(); + }, + [router] + ); + + const handleMessagesChange = useCallback( + async (_cId: string, _msgs: ChatUiMessage[]) => { + router.refresh(); + }, + [router] + ); + + const activeSession = currentSession ?? undefined; + + return ( + 0 || !!initialSession} + model={model} + isNewChat={isNewChat} + session={activeSession} + dashboardUrl={process.env.NEXT_PUBLIC_TRIGGER_DASHBOARD_URL} + projectDashboardPath={process.env.NEXT_PUBLIC_TRIGGER_PROJECT_DASHBOARD_PATH} + onFirstMessage={handleFirstMessage} + onMessagesChange={handleMessagesChange} + handoverEnabled={useHandover} + /> + ); +} diff --git a/references/ai-chat/src/components/chat.tsx b/references/ai-chat/src/components/chat.tsx new file mode 100644 index 00000000000..c4a37e258d4 --- /dev/null +++ b/references/ai-chat/src/components/chat.tsx @@ -0,0 +1,995 @@ +"use client"; + +import { useChat } from "@ai-sdk/react"; +import { + lastAssistantMessageIsCompleteWithApprovalResponses, + lastAssistantMessageIsCompleteWithToolCalls, +} from "ai"; +import type { ChatUiMessage } from "@/lib/chat-tools-schemas"; +import type { TriggerChatTransport } from "@trigger.dev/sdk/chat"; + +// Structural type mirroring @trigger.dev/sdk/ai's CompactionChunkData. +// Importing from the `ai` subpath drags the full chat.agent module — +// including skills' `node:child_process` import — into the client +// bundle. Keeping this inline is a type-only dependency. +type CompactionChunkData = { + status: "compacting" | "compacted"; + totalTokens?: number; +}; +import { usePendingMessages, useMultiTabChat } from "@trigger.dev/sdk/chat/react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Streamdown } from "streamdown"; +import { MODEL_OPTIONS } from "@/lib/models"; + +function ToolInvocation({ + part, + onApprove, + onDeny, + onToolOutput, +}: { + part: any; + onApprove?: (approvalId: string) => void; + onDeny?: (approvalId: string) => void; + onToolOutput?: (tool: string, toolCallId: string, output: unknown) => void; +}) { + const [expanded, setExpanded] = useState(false); + const toolName = part.type.startsWith("tool-") ? part.type.slice(5) : "tool"; + const state = part.state ?? "input-available"; + const args = part.input; + const result = part.output; + + const isLoading = state === "input-streaming" || state === "input-available"; + const isError = state === "output-error"; + const needsApproval = state === "approval-requested"; + const wasApproved = state === "approval-responded" && part.approval?.approved === true; + const wasDenied = state === "approval-responded" && part.approval?.approved === false; + + return ( +
+ + + {needsApproval && ( +
+ + +
+ )} + + {/* askUser tool: show question + option buttons when input-available */} + {toolName === "askUser" && state === "input-available" && args?.question && ( +
+
{args.question}
+
+ {(args.options ?? []).map((opt: any) => ( + + ))} +
+
+ )} + + {expanded && ( +
+ {args && Object.keys(args).length > 0 && ( +
+
Input
+
+                {JSON.stringify(args, null, 2)}
+              
+
+ )} + {state === "output-available" && result !== undefined && ( +
+
Output
+
+                {JSON.stringify(result, null, 2)}
+              
+
+ )} + {isError && result !== undefined && ( +
+
Error
+
+                {typeof result === "string" ? result : JSON.stringify(result, null, 2)}
+              
+
+ )} +
+ )} +
+ ); +} + +function ResearchProgress({ part }: { part: any }) { + const data = part.data as { + status: "fetching" | "done"; + query: string; + current: number; + total: number; + currentUrl?: string; + completedUrls: string[]; + }; + + const isDone = data.status === "done"; + + return ( +
+
+ {isDone ? ( + + ) : ( + + )} + + {isDone + ? `Research complete — ${data.total} sources fetched` + : `Researching "${data.query}" (${data.current}/${data.total})`} + +
+ {data.currentUrl && !isDone && ( +
Fetching {data.currentUrl}
+ )} + {data.completedUrls.length > 0 && ( +
+ {data.completedUrls.map((url, i) => ( +
+ ✓ {url} +
+ ))} +
+ )} +
+ ); +} + +type TtfbEntry = { turn: number; ttfbMs: number }; + +function DebugPanel({ + chatId, + model, + status, + session, + dashboardUrl, + projectDashboardPath, + messageCount, + ttfbHistory, +}: { + chatId: string; + model: string; + status: string; + session?: { publicAccessToken: string; lastEventId?: string; isStreaming?: boolean }; + dashboardUrl?: string; + projectDashboardPath?: string; + messageCount: number; + ttfbHistory: TtfbEntry[]; +}) { + const runsUrl = + dashboardUrl && projectDashboardPath + ? `${dashboardUrl}${projectDashboardPath}/env/dev/runs?tags=${encodeURIComponent(`chat:${chatId}`)}` + : undefined; + const [open, setOpen] = useState(false); + + const latestTtfb = ttfbHistory.length > 0 ? ttfbHistory[ttfbHistory.length - 1]! : undefined; + const avgTtfb = + ttfbHistory.length > 0 + ? Math.round(ttfbHistory.reduce((sum, e) => sum + e.ttfbMs, 0) / ttfbHistory.length) + : undefined; + + return ( +
+ + + {open && ( +
+ + + + + {runsUrl && } + {session ? ( + <> + + + + ) : ( + + )} + {ttfbHistory.length > 0 && ( + <> +
+ TTFB + {avgTtfb !== undefined && ( + avg {avgTtfb.toLocaleString()}ms + )} +
+ {ttfbHistory.map((entry) => ( +
+ Turn {entry.turn} + {entry.ttfbMs.toLocaleString()}ms +
+ ))} + + )} +
+ )} +
+ ); +} + +function Row({ + label, + value, + mono, + link, +}: { + label: string; + value: string; + mono?: boolean; + link?: string; +}) { + return ( +
+ ); +} + +type ChatProps = { + chatId: string; + initialMessages: ChatUiMessage[]; + transport: TriggerChatTransport; + resume?: boolean; + model: string; + isNewChat: boolean; + onModelChange?: (model: string) => void; + session?: { publicAccessToken: string; lastEventId?: string; isStreaming?: boolean }; + dashboardUrl?: string; + projectDashboardPath?: string; + onFirstMessage?: (chatId: string, text: string) => void; + onMessagesChange?: (chatId: string, messages: ChatUiMessage[]) => void; + /** Whether the transport is configured to route first-turn through `chat.handover`. */ + handoverEnabled?: boolean; +}; + +export function Chat({ + chatId, + initialMessages, + transport, + resume: resumeProp, + model, + isNewChat, + onModelChange, + session, + dashboardUrl, + projectDashboardPath, + onFirstMessage, + onMessagesChange, + handoverEnabled = false, +}: ChatProps) { + const [input, setInput] = useState(""); + const hasCalledFirstMessage = useRef(false); + + // TTFB tracking + const sendTimestamp = useRef(null); + const turnCounter = useRef(0); + const [ttfbHistory, setTtfbHistory] = useState([]); + + const { + messages, + setMessages, + sendMessage, + stop: aiStop, + addToolApprovalResponse, + addToolOutput, + regenerate, + status, + error, + } = useChat({ + id: chatId, + messages: initialMessages, + transport, + resume: resumeProp, + sendAutomaticallyWhen: (opts) => + lastAssistantMessageIsCompleteWithApprovalResponses(opts) || + lastAssistantMessageIsCompleteWithToolCalls(opts), + }); + + // Multi-tab coordination: sync messages between tabs + const { isReadOnly } = useMultiTabChat(transport, chatId, messages, setMessages); + + // Use transport.stopGeneration for reliable stop after reconnect. + // Once the AI SDK passes abortSignal through reconnectToStream, + // aiStop() alone will suffice. Until then, this covers both cases. + const stop = useCallback(() => { + transport.stopGeneration(chatId); + aiStop(); + }, [transport, chatId, aiStop]); + + // Tool approval callbacks + const handleApprove = useCallback( + (approvalId: string) => { + addToolApprovalResponse({ id: approvalId, approved: true }); + }, + [addToolApprovalResponse, chatId, messages, status] + ); + + const handleDeny = useCallback( + (approvalId: string) => { + addToolApprovalResponse({ id: approvalId, approved: false, reason: "User denied" }); + }, + [addToolApprovalResponse, chatId] + ); + + // Notify parent of first user message (for chat metadata creation) + useEffect(() => { + if (hasCalledFirstMessage.current) return; + const firstUser = messages.find((m) => m.role === "user"); + if (firstUser) { + hasCalledFirstMessage.current = true; + const text = firstUser.parts + .filter((p: any) => p.type === "text") + .map((p: any) => p.text) + .join(" "); + onFirstMessage?.(chatId, text); + } + }, [messages, chatId, onFirstMessage]); + + // TTFB detection: record when first assistant content appears after send + useEffect(() => { + if (status !== "streaming") return; + if (sendTimestamp.current === null) return; + const lastMsg = messages[messages.length - 1]; + if (lastMsg?.role === "assistant") { + const ttfbMs = Date.now() - sendTimestamp.current; + const turn = turnCounter.current; + sendTimestamp.current = null; + setTtfbHistory((prev) => [...prev, { turn, ttfbMs }]); + } + }, [status, messages]); + + // Pending messages — handles steering messages during streaming + const pending = usePendingMessages({ + transport, + chatId, + status, + messages, + setMessages, + sendMessage, + metadata: { model }, + }); + + // Expose test helpers for automated testing via Chrome DevTools. + // All actions go through refs so closures always call the latest version. + const stateRef = useRef({ status, messages, pending: pending.pending, error }); + stateRef.current = { status, messages, pending: pending.pending, error }; + + // Diagnostic: when the AI SDK transitions into an error state, log the + // root cause once. Useful for catching transient mid-flow errors that + // surface as `status: "error"` but leave no obvious clue otherwise. + const prevErrorRef = useRef(null); + useEffect(() => { + if (error && error !== prevErrorRef.current) { + // eslint-disable-next-line no-console + console.error("[chat.error]", { + message: error.message, + name: error.name, + stack: error.stack?.split("\n").slice(0, 6).join("\n"), + chatId, + status, + msgCount: messages.length, + lastEventId: transport.getSession(chatId)?.lastEventId ?? null, + }); + } + prevErrorRef.current = error; + }, [error, chatId, status, messages.length, transport]); + + const actionsRef = useRef({ + steer: pending.steer, + queue: pending.queue, + promote: pending.promoteToSteering, + send: (text: string) => { + turnCounter.current++; + sendTimestamp.current = Date.now(); + sendMessage({ text }, { metadata: { model } }); + }, + stop, + }); + actionsRef.current = { + steer: pending.steer, + queue: pending.queue, + promote: pending.promoteToSteering, + send: (text: string) => { + turnCounter.current++; + sendTimestamp.current = Date.now(); + sendMessage({ text }, { metadata: { model } }); + }, + stop, + }; + + useEffect(() => { + // ── Test bridge ────────────────────────────────────────────────── + // + // Exposes `window.__chat` so an automated driver (Chrome DevTools + // MCP, Playwright, etc.) can exercise the chat end-to-end without + // clicking buttons. The bridge is mounted only when this component + // is alive — unmount clears `window.__chat`, so each chat page owns + // the namespace. + // + // Bridge surface groups: + // + // - **State accessors** (always fresh via refs): `status`, `messages`, + // `pending`, `chatId`, plus the full `session` object (sessionId, + // runId, lastEventId, isStreaming) and its convenience unwraps + // `sessionId` / `runId` / `lastEventId`. + // - **Actions**: `send`, `stop`, `steer`, `queue`, `promote`, and + // `setMessages` so scripts can inject fixture state for + // refresh-replay tests. `stop` calls both `transport.stopGeneration` + // and `aiStop()` — same surface the UI's Stop button uses. + // - **Waiters** — resolve a Promise when an async condition holds: + // `waitForStatus(target, timeoutMs)`, + // `waitForMessage(predicate, timeoutMs)`, + // `waitForFirstAssistantText(timeoutMs)` (convenience — resolves + // once any assistant message has a non-empty text part), + // `steerOnToolCall(text)`. Default timeout 30s; rejects on timeout. + // - **Scripted helpers** (`steerAfterDelay`, `queueAfterDelay`) for + // fire-at-time side effects. + // + // Waiters poll at 50ms. That's tight enough for text-delta races + // and light enough that the React tree doesn't feel it. + const POLL_MS = 50; + const DEFAULT_TIMEOUT_MS = 30_000; + + const waitFor = ( + check: () => T | false | null | undefined, + timeoutMs = DEFAULT_TIMEOUT_MS, + label = "condition" + ): Promise => + new Promise((resolve, reject) => { + const start = Date.now(); + const immediate = check(); + if (immediate) { + resolve(immediate as T); + return; + } + const interval = setInterval(() => { + const got = check(); + if (got) { + clearInterval(interval); + resolve(got as T); + return; + } + if (Date.now() - start > timeoutMs) { + clearInterval(interval); + reject(new Error(`__chat: timed out after ${timeoutMs}ms waiting for ${label}`)); + } + }, POLL_MS); + }); + + (window as any).__chat = { + // ── State ───────────────────────────────────────────────────── + get status() { + return stateRef.current.status; + }, + get messages() { + return stateRef.current.messages; + }, + get pending() { + return stateRef.current.pending; + }, + get session() { + // Live session state from the transport (sessionId, runId, + // lastEventId, isStreaming). Falls back to the SSR-supplied + // session for runs the transport hasn't observed yet. + return transport.getSession(chatId) ?? session ?? null; + }, + get sessionId() { + // Sessions-as-run-manager: sessionId is no longer surfaced + // through the transport. The chat is addressed by `chatId` and + // any consumer that wants the friendlyId must look it up via + // `sessions.retrieve(chatId)` server-side. + return null; + }, + get runId() { + // Sessions-as-run-manager: runs come and go inside the Session + // and are managed server-side. The transport doesn't track the + // live runId anymore; consumers wanting it should query + // `sessions.retrieve(chatId)` server-side. + return null; + }, + get lastEventId() { + return transport.getSession(chatId)?.lastEventId ?? null; + }, + get error() { + // Surface the AI SDK's last error so smoke tests can capture the + // root cause when status flips to "error" mid-flow. + const err = stateRef.current.error; + if (!err) return null; + return { + message: (err as Error).message, + name: (err as Error).name, + stack: (err as Error).stack?.split("\n").slice(0, 6).join("\n"), + }; + }, + chatId, + /** True when the transport is configured to route first-turn through `chat.handover`. */ + handoverEnabled, + + // ── Actions ─────────────────────────────────────────────────── + steer: (text: string) => actionsRef.current.steer(text), + queue: (text: string) => actionsRef.current.queue(text), + promote: (id: string) => actionsRef.current.promote(id), + send: (text: string) => actionsRef.current.send(text), + stop: () => actionsRef.current.stop(), + sendAction: (action: unknown) => transport.sendAction(chatId, action), + regenerate: () => regenerate(), + + // ── Waiters ─────────────────────────────────────────────────── + waitForStatus: (target: string, timeoutMs = DEFAULT_TIMEOUT_MS) => + waitFor( + () => (stateRef.current.status === target ? (true as const) : false), + timeoutMs, + `status === "${target}" (current: "${stateRef.current.status}")` + ), + waitForMessage: ( + predicate: (m: any, all: any[]) => boolean, + timeoutMs = DEFAULT_TIMEOUT_MS + ): Promise => + waitFor( + () => stateRef.current.messages.find((m) => predicate(m, stateRef.current.messages)) as M | undefined, + timeoutMs, + "matching message" + ), + waitForFirstAssistantText: (timeoutMs = DEFAULT_TIMEOUT_MS) => + waitFor( + () => { + const assistant = stateRef.current.messages.find((m) => m.role === "assistant"); + if (!assistant) return false; + const text = (assistant.parts ?? []) + .filter((p: any) => p.type === "text" && typeof p.text === "string" && p.text.length > 0) + .map((p: any) => p.text) + .join(""); + return text.length > 0 ? { id: assistant.id, text } : false; + }, + timeoutMs, + "first assistant text" + ), + steerOnToolCall: (text: string, timeoutMs = DEFAULT_TIMEOUT_MS) => + waitFor( + () => { + const lastMsg = stateRef.current.messages[stateRef.current.messages.length - 1]; + const hasTool = + lastMsg?.role === "assistant" && + lastMsg.parts?.some((p: any) => p.type?.startsWith("tool-")); + if (!hasTool) return false; + actionsRef.current.steer(text); + return true as const; + }, + timeoutMs, + "tool call" + ), + + // ── Scripted helpers ───────────────────────────────────────── + steerAfterDelay: (text: string, ms: number) => + new Promise((r) => + setTimeout(() => { + actionsRef.current.steer(text); + r(); + }, ms) + ), + queueAfterDelay: (text: string, ms: number) => + new Promise((r) => + setTimeout(() => { + actionsRef.current.queue(text); + r(); + }, ms) + ), + }; + return () => { + delete (window as any).__chat; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [chatId]); + + // Persist messages when a turn completes + const prevStatus = useRef(status); + useEffect(() => { + const turnCompleted = prevStatus.current === "streaming" && status === "ready"; + prevStatus.current = status; + if (!turnCompleted) return; + if (messages.length > 0) { + onMessagesChange?.(chatId, messages); + } + }, [status, messages, chatId, onMessagesChange]); + + return ( +
+ {/* Model selector for new chats */} + {isNewChat && messages.length === 0 && onModelChange && ( +
+ Model: + +
+ )} + + {/* Model badge for existing chats */} + {(!isNewChat || messages.length > 0) && ( +
+ + {model} + +
+ )} + + {/* Messages */} +
+ {messages.length === 0 && ( +

+ Send a message to start chatting. +

+ )} + + {messages.map((message) => ( +
+
+
+ {message.parts.map((part, i) => { + if (part.type === "text") { + if (message.role === "assistant") { + return {part.text}; + } + return {part.text}; + } + + if (part.type === "reasoning") { + return ( +
+ + Thinking... + +
+ {part.text} +
+
+ ); + } + + // Transient status parts — hide from rendered output + if ( + part.type === "data-turn-status" || + part.type === "data-background-context-injected" + ) { + return null; + } + + if (part.type === "data-research-progress") { + return ; + } + + if (part.type === "data-compaction") { + const data = (part as any).data as CompactionChunkData; + return ( +
+ {data.status === "compacting" ? "⏳" : "✂️"} + + {data.status === "compacting" + ? `Compacting conversation${ + data.totalTokens + ? ` (${data.totalTokens.toLocaleString()} tokens)` + : "" + }...` + : "Conversation compacted"} + +
+ ); + } + + if (part.type.startsWith("tool-")) { + return ( + + addToolOutput({ tool, toolCallId, output }) + } + /> + ); + } + + if (pending.isInjectionPoint(part)) { + const injectedMsgs = pending.getInjectedMessages(part); + if (injectedMsgs.length === 0) return null; + return ( +
+
+ {injectedMsgs.map((m) => ( +
+ {m.text} +
+ ))} +
+ injected mid-response +
+
+
+ ); + } + + if (part.type.startsWith("data-")) { + return ( +
+ {part.type} +
+                          {JSON.stringify((part as any).data, null, 2)}
+                        
+
+ ); + } + + return null; + })} +
+
+
+ ))} + + {status === "streaming" && messages[messages.length - 1]?.role !== "assistant" && ( +
+
+ Thinking... +
+
+ )} + + {pending.pending.map((msg) => ( +
+
+
+ {msg.text} +
+
+ + {msg.mode === "steering" + ? "Steering — waiting for injection point" + : "Queued for next turn"} + + {msg.mode === "queued" && status === "streaming" && ( + + )} +
+
+
+ ))} +
+ + {error && ( +
+ {error.message} +
+ )} + + {/* Debug panel */} + + +
{ + e.preventDefault(); + if (!input.trim()) return; + if (status !== "streaming") { + turnCounter.current++; + sendTimestamp.current = Date.now(); + } + pending.steer(input); + setInput(""); + }} + className="shrink-0 border-t border-gray-200 bg-white p-4" + > + {isReadOnly && ( +
+ This chat is active in another tab. Messages are read-only. +
+ )} +
+ setInput(e.target.value)} + placeholder={isReadOnly ? "Chat is active in another tab" : "Type a message..."} + disabled={isReadOnly} + className="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:text-gray-400" + /> + + {/* Preload — only visible before the first message lands. After + the user sends, the transport creates the session lazily, so + session becomes truthy and this button hides itself. The + transport tracks an in-flight preload internally; double-clicks + are a no-op. */} + {messages.length === 0 && !session && ( + + )} + {status === "streaming" && ( + + )} + {status === "streaming" && ( + + )} + {/* Undo — server-side `chat.history.slice(0, -2)` via the + `undo` action, optimistically reflected in the local + `useChat` state. Drops the last user / assistant exchange. + Only meaningful when there's at least one full exchange + and the chat isn't currently streaming. */} + {status !== "streaming" && messages.length >= 2 && ( + + )} +
+
+
+ ); +} diff --git a/references/ai-chat/src/lib/chat-tools-schemas.ts b/references/ai-chat/src/lib/chat-tools-schemas.ts new file mode 100644 index 00000000000..2def5f68553 --- /dev/null +++ b/references/ai-chat/src/lib/chat-tools-schemas.ts @@ -0,0 +1,189 @@ +/** + * Schema-only tool definitions — shared between the chat.handover + * route handler and the trigger.dev agent task. + * + * ⚠️ HARD CONSTRAINT — bundle isolation + * + * This file is imported by `app/api/chat/route.ts` (the chat.handover + * POST handler) and runs in the Next.js process. Anything imported + * here lands in the route-handler bundle. + * + * Allowed imports: `ai` (for `tool()`), `zod`, type-only AI SDK + * imports. Nothing else. + * + * DO NOT import from this file: + * - `@e2b/code-interpreter`, `puppeteer`, `playwright`, native bindings + * - `node:child_process`, heavy filesystem ops + * - `@trigger.dev/sdk` runtime (`task`, `schemaTask`, + * `chat.stream.writer`, etc. — pulls in the whole task runtime) + * - `turndown`, image processing libs, anything that pulls weight + * + * Heavy `execute` fns live in `src/trigger/chat-tools.ts` — that file + * imports these schemas and adds executes on top. The agent task + * picks up the executes when it runs; the route handler never sees + * them and never imports their deps. + * + * If you need to add a new tool to the chat.agent's schema-only set, + * declare its description + inputSchema here, then wire its execute + * fn in `src/trigger/chat-tools.ts`. + */ +import { tool } from "ai"; +import type { InferUITools, UIDataTypes, UIMessage } from "ai"; +import { z } from "zod"; + +export const inspectEnvironment = tool({ + description: + "Inspect the current execution environment. Returns runtime info (Node.js/Bun/Deno version), " + + "OS details, CPU architecture, memory usage, environment variables, and platform metadata.", + inputSchema: z.object({}), + // execute → src/trigger/chat-tools.ts +}); + +export const webFetch = tool({ + description: + "Fetch a URL and return the response as text. " + + "Use this to retrieve web pages, APIs, or any HTTP resource.", + inputSchema: z.object({ + url: z.string().url().describe("The URL to fetch"), + }), + // execute → src/trigger/chat-tools.ts (uses turndown) +}); + +export const deepResearch = tool({ + description: + "Research a topic by fetching multiple URLs and synthesizing the results. " + + "Streams progress updates to the chat as it works.", + inputSchema: z.object({ + query: z.string().describe("The research query or topic"), + urls: z.array(z.string().url()).describe("URLs to fetch and analyze"), + }), + // execute → src/trigger/chat-tools.ts (subtask via ai.toolExecute) +}); + +export const posthogQuery = tool({ + description: + "Query PostHog analytics using HogQL. Use this to answer questions about events, " + + "pageviews, user activity, feature flag usage, or any product analytics question. " + + "Write a HogQL query (SQL-like syntax over PostHog events).", + inputSchema: z.object({ + query: z + .string() + .describe( + "HogQL query, e.g. SELECT event, count() FROM events WHERE timestamp > now() - interval 1 day GROUP BY event ORDER BY count() DESC LIMIT 10" + ), + }), + // execute → src/trigger/chat-tools.ts (HTTP to PostHog) +}); + +export const executeCode = tool({ + description: + "Run code in an isolated E2B sandbox (Python by default; other languages supported by E2B). " + + "Use for calculations, data analysis, or transforming tool outputs (e.g. PostHog query results). " + + "The sandbox persists across turns in the same run until the chat idles and suspends.", + inputSchema: z.object({ + code: z.string().describe("Source code to execute in the sandbox"), + language: z + .string() + .optional() + .describe("Language id (e.g. python, javascript). Defaults to python."), + }), + // execute → src/trigger/chat-tools.ts (E2B sandbox — heavy native dep) +}); + +export const sendEmail = tool({ + description: + "Send an email to a recipient. Requires human approval before sending. " + + "Use when the user asks you to send, draft, or compose an email.", + inputSchema: z.object({ + to: z.string().describe("Recipient email address"), + subject: z.string().describe("Email subject line"), + body: z.string().describe("Email body text"), + }), + needsApproval: true, + // execute → src/trigger/chat-tools.ts +}); + +export const askUser = tool({ + description: + "Ask the user a question when you need clarification or input before proceeding. " + + "Present 2-4 options for the user to choose from. Use when uncertain about the user's intent.", + inputSchema: z.object({ + question: z.string().describe("The question to ask the user"), + options: z + .array( + z.object({ + id: z.string().describe("Unique option identifier"), + label: z.string().describe("Short option title"), + description: z.string().optional().describe("Longer explanation"), + }) + ) + .min(2) + .max(4), + }), + // No execute by design — round-tripped through the frontend's addToolOutput. +}); + +export const getCurrentTime = tool({ + description: + "Get the current wall-clock date and time. Returns ISO timestamp, " + + "human-readable strings, and the system timezone. Use when the user " + + "asks 'what time is it', for date math, or to anchor 'recent' / 'today'.", + inputSchema: z.object({}), + // execute → src/trigger/chat-tools.ts +}); + +export const searchHackerNews = tool({ + description: + "Search Hacker News for stories matching a query, or fetch the current top stories. " + + "Returns title, points, comment count, author, posted-at, and URL for up to 10 results. " + + "Use for tech news, trending topics, or 'what's everyone talking about'.", + inputSchema: z.object({ + query: z + .string() + .optional() + .describe( + "Search query. If omitted, returns the current top stories instead of doing a search." + ), + limit: z.number().int().min(1).max(10).optional().describe("Max results (1-10, default 5)"), + }), + // execute → src/trigger/chat-tools.ts +}); + +export const createGithubIssue = tool({ + description: + "Create a GitHub issue tracking action items, bugs, or follow-ups. " + + "Requires human approval before creation. Use when the user asks " + + "to file an issue, track a bug, or open a ticket.", + inputSchema: z.object({ + repo: z + .string() + .describe("Repository in 'owner/name' form (e.g. 'triggerdotdev/trigger.dev')"), + title: z.string().describe("Issue title"), + body: z.string().describe("Issue body in Markdown"), + labels: z.array(z.string()).optional().describe("Labels to apply (e.g. ['bug', 'p1'])"), + }), + needsApproval: true, + // execute → src/trigger/chat-tools.ts +}); + +/** + * The schema-only tool set passed to `chat.headStart`'s `streamText` + * call. The agent task imports each schema individually and adds the + * matching `execute` fn — see `src/trigger/chat-tools.ts`. + */ +export const headStartTools = { + inspectEnvironment, + webFetch, + deepResearch, + posthogQuery, + executeCode, + sendEmail, + askUser, + getCurrentTime, + searchHackerNews, + createGithubIssue, +}; + +type ChatToolSet = typeof headStartTools; +export type ChatUiTools = InferUITools; +export type ChatUiMessage = UIMessage; diff --git a/references/ai-chat/src/lib/chat-tools.ts b/references/ai-chat/src/lib/chat-tools.ts new file mode 100644 index 00000000000..8277ca4d9fa --- /dev/null +++ b/references/ai-chat/src/lib/chat-tools.ts @@ -0,0 +1,337 @@ +import { ai, chat } from "@trigger.dev/sdk/ai"; +import { schemaTask } from "@trigger.dev/sdk"; +import { tool, generateId } from "ai"; +import type { InferUITools, UIDataTypes, UIMessage } from "ai"; +import { z } from "zod"; +import os from "node:os"; +import TurndownService from "turndown"; +import { codeSandboxRun, runWithCodeSandbox } from "@/lib/code-sandbox"; + +const turndown = new TurndownService(); + +// Silence TS errors for Bun/Deno global checks +declare const Bun: unknown; +declare const Deno: unknown; + +export const inspectEnvironment = tool({ + description: + "Inspect the current execution environment. Returns runtime info (Node.js/Bun/Deno version), " + + "OS details, CPU architecture, memory usage, environment variables, and platform metadata.", + inputSchema: z.object({}), + execute: async () => { + const memUsage = process.memoryUsage(); + + return { + runtime: { + name: typeof Bun !== "undefined" ? "bun" : typeof Deno !== "undefined" ? "deno" : "node", + version: process.version, + versions: { + v8: process.versions.v8, + openssl: process.versions.openssl, + modules: process.versions.modules, + }, + }, + os: { + platform: process.platform, + arch: process.arch, + release: os.release(), + type: os.type(), + hostname: os.hostname(), + uptime: `${Math.floor(os.uptime())}s`, + }, + cpus: { + count: os.cpus().length, + model: os.cpus()[0]?.model, + }, + memory: { + total: `${Math.round(os.totalmem() / 1024 / 1024)}MB`, + free: `${Math.round(os.freemem() / 1024 / 1024)}MB`, + process: { + rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`, + heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, + heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, + }, + }, + env: { + NODE_ENV: process.env.NODE_ENV, + TZ: process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone, + LANG: process.env.LANG, + }, + process: { + pid: process.pid, + cwd: process.cwd(), + execPath: process.execPath, + argv: process.argv.slice(0, 3), + }, + }; + }, +}); + +export const webFetch = tool({ + description: + "Fetch a URL and return the response as text. " + + "Use this to retrieve web pages, APIs, or any HTTP resource.", + inputSchema: z.object({ + url: z.string().url().describe("The URL to fetch"), + }), + execute: async ({ url }) => { + const latency = Number(process.env.WEBFETCH_LATENCY_MS); + if (latency > 0) { + await new Promise((r) => setTimeout(r, latency)); + } + + const response = await fetch(url); + let text = await response.text(); + const contentType = response.headers.get("content-type") ?? ""; + + if (contentType.includes("html")) { + text = turndown.turndown(text); + } + + return { + status: response.status, + contentType, + body: text.slice(0, 2000), + truncated: text.length > 2000, + }; + }, +}); + +const deepResearchTask = schemaTask({ + id: "deep-research", + description: + "Research a topic by fetching multiple URLs and synthesizing the results. " + + "Streams progress updates to the chat as it works.", + schema: z.object({ + query: z.string().describe("The research query or topic"), + urls: z.array(z.string().url()).describe("URLs to fetch and analyze"), + }), + run: async ({ query, urls }) => { + const partId = generateId(); + const results: { url: string; status: number; snippet: string }[] = []; + + for (let i = 0; i < urls.length; i++) { + const url = urls[i]!; + + const { waitUntilComplete } = chat.stream.writer({ + target: "root", + execute: ({ write }) => { + write({ + type: "data-research-progress", + id: partId, + data: { + status: "fetching" as const, + query, + current: i + 1, + total: urls.length, + currentUrl: url, + completedUrls: results.map((r) => r.url), + }, + }); + }, + }); + await waitUntilComplete(); + + try { + const response = await fetch(url); + let text = await response.text(); + const contentType = response.headers.get("content-type") ?? ""; + + if (contentType.includes("html")) { + text = turndown.turndown(text); + } + + results.push({ + url, + status: response.status, + snippet: text.slice(0, 500), + }); + } catch (err) { + results.push({ + url, + status: 0, + snippet: `Error: ${err instanceof Error ? err.message : String(err)}`, + }); + } + } + + const { waitUntilComplete: waitForDone } = chat.stream.writer({ + target: "root", + execute: ({ write }) => { + write({ + type: "data-research-progress", + id: partId, + data: { + status: "done" as const, + query, + current: urls.length, + total: urls.length, + completedUrls: results.map((r) => r.url), + }, + }); + }, + }); + await waitForDone(); + + return { query, results }; + }, +}); + +/** Task-backed tool: AI SDK `tool()` for shape/types; `ai.toolExecute` for Trigger subtask + metadata. */ +export const deepResearch = tool({ + description: deepResearchTask.description ?? "", + inputSchema: deepResearchTask.schema!, + execute: ai.toolExecute(deepResearchTask), +}); + +const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY; +const POSTHOG_PROJECT_ID = process.env.POSTHOG_PROJECT_ID; +const POSTHOG_HOST = process.env.POSTHOG_HOST ?? "https://eu.posthog.com"; + +export const posthogQuery = tool({ + description: + "Query PostHog analytics using HogQL. Use this to answer questions about events, " + + "pageviews, user activity, feature flag usage, or any product analytics question. " + + "Write a HogQL query (SQL-like syntax over PostHog events).", + inputSchema: z.object({ + query: z + .string() + .describe( + "HogQL query, e.g. SELECT event, count() FROM events WHERE timestamp > now() - interval 1 day GROUP BY event ORDER BY count() DESC LIMIT 10" + ), + }), + execute: async ({ query }) => { + if (!POSTHOG_API_KEY || !POSTHOG_PROJECT_ID) { + return { error: "PostHog not configured. Set POSTHOG_API_KEY and POSTHOG_PROJECT_ID." }; + } + const response = await fetch(`${POSTHOG_HOST}/api/projects/${POSTHOG_PROJECT_ID}/query/`, { + method: "POST", + headers: { + Authorization: `Bearer ${POSTHOG_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ query: { kind: "HogQLQuery", query } }), + }); + + if (!response.ok) { + const text = await response.text(); + return { error: `PostHog API error ${response.status}: ${text.slice(0, 500)}` }; + } + + const data = await response.json(); + return { + columns: data.columns, + results: data.results?.slice(0, 50), + rowCount: data.results?.length ?? 0, + }; + }, +}); + +export const executeCode = tool({ + description: + "Run code in an isolated E2B sandbox (Python by default; other languages supported by E2B). " + + "Use for calculations, data analysis, or transforming tool outputs (e.g. PostHog query results). " + + "The sandbox persists across turns in the same run until the chat idles and suspends.", + inputSchema: z.object({ + code: z.string().describe("Source code to execute in the sandbox"), + language: z + .string() + .optional() + .describe("Language id (e.g. python, javascript). Defaults to python."), + }), + execute: async function executeCodeExecute({ code, language }) { + const runId = codeSandboxRun.runId; + if (!runId?.trim()) { + return { + error: + "Code sandbox run id is not set yet (call from the chat task after onTurnStart), or this tool is not wired to that task.", + }; + } + + const out = await runWithCodeSandbox(runId, async function runInSandbox(sandbox) { + const execution = await sandbox.runCode(code, { + ...(language?.trim() ? { language: language.trim() } : {}), + timeoutMs: 60_000, + }); + + if (execution.error) { + return { + error: `${execution.error.name}: ${execution.error.value}`, + traceback: execution.error.traceback, + stdout: execution.logs.stdout.join("\n"), + stderr: execution.logs.stderr.join("\n"), + }; + } + + const mainText = execution.text; + const resultSnippets = execution.results + .map(function mapResult(r) { + return r.text ?? r.markdown ?? r.json; + }) + .filter(Boolean) + .slice(0, 5); + + return { + text: mainText, + results: resultSnippets, + stdout: execution.logs.stdout.join("\n"), + stderr: execution.logs.stderr.join("\n"), + }; + }); + + return out; + }, +}); + +export const sendEmail = tool({ + description: + "Send an email to a recipient. Requires human approval before sending. " + + "Use when the user asks you to send, draft, or compose an email.", + inputSchema: z.object({ + to: z.string().describe("Recipient email address"), + subject: z.string().describe("Email subject line"), + body: z.string().describe("Email body text"), + }), + needsApproval: true, + execute: async ({ to, subject, body }) => { + // Simulated — in a real app this would call an email API + return { sent: true, to, subject, preview: body.slice(0, 100) }; + }, +}); + +export const askUser = tool({ + description: + "Ask the user a question when you need clarification or input before proceeding. " + + "Present 2-4 options for the user to choose from. Use when uncertain about the user's intent.", + inputSchema: z.object({ + question: z.string().describe("The question to ask the user"), + options: z + .array( + z.object({ + id: z.string().describe("Unique option identifier"), + label: z.string().describe("Short option title"), + description: z.string().optional().describe("Longer explanation"), + }) + ) + .min(2) + .max(4), + }), + // No execute function — streamText ends, turn completes, + // frontend sends the answer via addToolOutput +}); + +/** Tool set passed to `streamText` for the main `chat.agent` run (includes PostHog). */ +export const chatTools = { + inspectEnvironment, + webFetch, + deepResearch, + posthogQuery, + executeCode, + sendEmail, + askUser, +}; + +type ChatToolSet = typeof chatTools; + +export type ChatUiTools = InferUITools; +export type ChatUiMessage = UIMessage; diff --git a/references/ai-chat/src/lib/code-sandbox.ts b/references/ai-chat/src/lib/code-sandbox.ts new file mode 100644 index 00000000000..5a3e48cd6df --- /dev/null +++ b/references/ai-chat/src/lib/code-sandbox.ts @@ -0,0 +1,57 @@ +/** + * E2B sandboxes keyed by Trigger run id. + * + * - Warmed from `chat.agent` `onTurnStart` (non-blocking) so the first `executeCode` tool call is faster. + * - Disposed in `onChatSuspend` before the run suspends waiting for the next message. + * - `onComplete` disposes any leftover sandbox if the run ends without hitting another suspend. + * + * No extra SDK hook is required beyond `onChatSuspend` and `onComplete`. + */ +import { chat } from "@trigger.dev/sdk/ai"; +import { Sandbox } from "@e2b/code-interpreter"; + +const sandboxPromises = new Map>(); + +/** Run id for the active chat turn — set from `onTurnStart` so tools can key the sandbox without `taskContext`. */ +export const codeSandboxRun = chat.local<{ runId: string }>({ id: "codeSandboxRun" }); + +export function warmCodeSandbox(runId: string): void { + codeSandboxRun.init({ runId }); + if (!process.env.E2B_API_KEY?.trim()) return; + if (sandboxPromises.has(runId)) return; + sandboxPromises.set(runId, Sandbox.create()); +} + +export async function runWithCodeSandbox( + runId: string, + runner: (sandbox: Sandbox) => Promise +): Promise { + if (!process.env.E2B_API_KEY?.trim()) { + return { error: "Code sandbox not configured. Set E2B_API_KEY in the Trigger environment." }; + } + + let promise = sandboxPromises.get(runId); + if (!promise) { + promise = Sandbox.create(); + sandboxPromises.set(runId, promise); + } + + try { + const sandbox = await promise; + return await runner(sandbox); + } catch (err) { + return { error: err instanceof Error ? err.message : String(err) }; + } +} + +export async function disposeCodeSandboxForRun(runId: string): Promise { + const promise = sandboxPromises.get(runId); + if (!promise) return; + sandboxPromises.delete(runId); + try { + const sandbox = await promise; + await sandbox.kill(); + } catch { + /* best-effort cleanup */ + } +} diff --git a/references/ai-chat/src/lib/models.ts b/references/ai-chat/src/lib/models.ts new file mode 100644 index 00000000000..77c2ea1621d --- /dev/null +++ b/references/ai-chat/src/lib/models.ts @@ -0,0 +1,10 @@ +export const MODEL_OPTIONS = [ + "gpt-4o-mini", + "gpt-4o", + "claude-sonnet-4-6", + "claude-opus-4-6", +]; + +export const DEFAULT_MODEL = "claude-sonnet-4-6"; + +export const REASONING_MODELS = new Set(["claude-opus-4-6"]); diff --git a/references/ai-chat/src/lib/pr-review-helpers.ts b/references/ai-chat/src/lib/pr-review-helpers.ts new file mode 100644 index 00000000000..1bfb082f13d --- /dev/null +++ b/references/ai-chat/src/lib/pr-review-helpers.ts @@ -0,0 +1,81 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { rm } from "node:fs/promises"; +import { logger } from "@trigger.dev/sdk"; + +const execFileAsync = promisify(execFile); + +// #region git helper +export async function git(cwd: string, ...args: string[]): Promise { + const { stdout } = await execFileAsync("git", args, { + cwd, + maxBuffer: 10 * 1024 * 1024, // 10MB for large diffs + timeout: 30_000, + }); + return stdout.trim(); +} +// #endregion + +// #region GitHub API helper +export async function githubApi(path: string, token?: string | null): Promise { + const res = await fetch(`https://api.github.com${path}`, { + headers: { + Accept: "application/vnd.github.v3+json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`GitHub API ${res.status}: ${text.slice(0, 500)}`); + } + return res.json() as Promise; +} +// #endregion + +// #region URL parser +export function parseGitHubUrl(url: string): { owner: string; repo: string } { + const match = url.match(/github\.com\/([^/]+)\/([^/]+)/); + if (!match) throw new Error(`Invalid GitHub URL: ${url}`); + return { owner: match[1]!, repo: match[2]!.replace(/\.git$/, "") }; +} +// #endregion + +// #region Clone repo +export async function cloneRepo({ + owner, + repo, + clonePath, + token, +}: { + owner: string; + repo: string; + clonePath: string; + token?: string | null; +}): Promise { + async function runClone(): Promise { + const cloneUrl = token + ? `https://x-access-token:${token}@github.com/${owner}/${repo}.git` + : `https://github.com/${owner}/${repo}.git`; + + await execFileAsync("git", ["clone", "--depth=1", cloneUrl, clonePath], { + timeout: 60_000, + }); + } + + await logger.trace("cloneRepo", runClone, { + icon: "tabler-brand-github", + }); +} +// #endregion + +// #region Cleanup +export async function cleanupClone(clonePath: string | undefined): Promise { + if (!clonePath) return; + try { + await rm(clonePath, { recursive: true, force: true }); + logger.info("Cleaned up clone directory", { clonePath }); + } catch { + /* best-effort */ + } +} +// #endregion diff --git a/references/ai-chat/src/lib/pr-review-tools.ts b/references/ai-chat/src/lib/pr-review-tools.ts new file mode 100644 index 00000000000..31efe335588 --- /dev/null +++ b/references/ai-chat/src/lib/pr-review-tools.ts @@ -0,0 +1,191 @@ +import { chat } from "@trigger.dev/sdk/ai"; +import { logger } from "@trigger.dev/sdk"; +import { tool } from "ai"; +import type { InferUITools, UIDataTypes, UIMessage } from "ai"; +import { z } from "zod"; +import { resolve } from "node:path"; +import { readFile as fsReadFile } from "node:fs/promises"; +import { git, githubApi } from "@/lib/pr-review-helpers"; + +// #region Repo context — shared across tools, survives snapshot/restore +export const repo = chat.local<{ + cwd: string; + owner: string; + repo: string; + githubToken: string | null; + openPRs: Array<{ + number: number; + title: string; + author: string; + headBranch: string; + }>; + activePR: { number: number; headBranch: string } | null; +}>({ id: "repo" }); +// #endregion + +// #region Tool: Fetch PR +export const fetchPR = tool({ + description: + "Fetch a pull request by number. Checks out the PR branch in the local clone " + + "and returns metadata, the diff against the base branch, and the list of changed files. " + + "Always call this before reviewing a PR.", + inputSchema: z.object({ + prNumber: z.number().describe("The PR number to fetch"), + }), + execute: async ({ prNumber }) => { + const { cwd, owner, repo: repoName, githubToken } = repo; + + logger.info("fetchPR: fetching metadata", { owner, repo: repoName, prNumber }); + + // 1. Fetch PR metadata from GitHub API + const pr = await githubApi<{ + title: string; + body: string | null; + head: { ref: string; sha: string }; + base: { ref: string }; + user: { login: string }; + additions: number; + deletions: number; + changed_files: number; + }>(`/repos/${owner}/${repoName}/pulls/${prNumber}`, githubToken); + + logger.info("fetchPR: got PR metadata", { + title: pr.title, + head: pr.head.ref, + base: pr.base.ref, + author: pr.user.login, + }); + + // 2. Fetch the PR branch and check it out (must happen before fetching + // the base branch, since base is currently checked out after clone) + logger.info("fetchPR: fetching head branch", { branch: pr.head.ref, cwd }); + await git(cwd, "fetch", "origin", `${pr.head.ref}:${pr.head.ref}`); + + logger.info("fetchPR: checking out head branch", { branch: pr.head.ref }); + await git(cwd, "checkout", pr.head.ref); + + // 3. Now fetch the base branch (safe because we're no longer on it) + logger.info("fetchPR: fetching base branch", { branch: pr.base.ref }); + await git(cwd, "fetch", "origin", `${pr.base.ref}:${pr.base.ref}`); + + // 4. Get the diff + logger.info("fetchPR: computing diff", { range: `${pr.base.ref}...${pr.head.ref}` }); + const diff = await git( + cwd, + "diff", + `${pr.base.ref}...${pr.head.ref}` + ); + + // 5. Get changed files list + const filesRaw = await git( + cwd, + "diff", + "--name-status", + `${pr.base.ref}...${pr.head.ref}` + ); + const changedFiles = filesRaw + .split("\n") + .filter(Boolean) + .map((line) => { + const [status, ...pathParts] = line.split("\t"); + return { status: status!, path: pathParts.join("\t") }; + }); + + // 6. Update active PR state + repo.activePR = { number: prNumber, headBranch: pr.head.ref }; + + logger.info("fetchPR: done", { + changedFileCount: changedFiles.length, + diffLength: diff.length, + diffTruncated: diff.length > 50_000, + }); + + // 7. Truncate diff if too large + const maxDiffLength = 50_000; + const truncated = diff.length > maxDiffLength; + + return { + number: prNumber, + title: pr.title, + body: pr.body?.slice(0, 2000) ?? "(no description)", + author: pr.user.login, + headBranch: pr.head.ref, + baseBranch: pr.base.ref, + additions: pr.additions, + deletions: pr.deletions, + changedFileCount: pr.changed_files, + changedFiles, + diff: truncated ? diff.slice(0, maxDiffLength) : diff, + diffTruncated: truncated, + }; + }, +}); +// #endregion + +// #region Tool: Read File +export const readFile = tool({ + description: + "Read a file from the cloned repository. Use this to see full file context " + + "beyond what the diff shows — essential for understanding surrounding code, " + + "imports, type definitions, and related functions.", + inputSchema: z.object({ + path: z.string().describe("File path relative to the repo root"), + startLine: z + .number() + .optional() + .describe("Start reading from this line (1-indexed)"), + endLine: z + .number() + .optional() + .describe("Stop reading at this line (inclusive)"), + }), + execute: async ({ path: filePath, startLine, endLine }) => { + const { cwd } = repo; + const fullPath = `${cwd}/${filePath}`; + + // Security: ensure path doesn't escape the clone directory + const resolved = resolve(fullPath); + if (!resolved.startsWith(resolve(cwd))) { + return { error: "Path traversal not allowed" }; + } + + try { + const content = await fsReadFile(resolved, "utf-8"); + const lines = content.split("\n"); + + if (startLine || endLine) { + const start = (startLine ?? 1) - 1; + const end = endLine ?? lines.length; + const slice = lines.slice(start, end); + return { + path: filePath, + startLine: start + 1, + endLine: Math.min(end, lines.length), + totalLines: lines.length, + content: slice.join("\n"), + }; + } + + const maxLines = 500; + return { + path: filePath, + totalLines: lines.length, + content: lines.slice(0, maxLines).join("\n"), + truncated: lines.length > maxLines, + }; + } catch (err) { + return { + error: `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`, + }; + } + }, +}); +// #endregion + +// #region Exports +export const prReviewTools = { fetchPR, readFile }; + +type PRReviewToolSet = typeof prReviewTools; +export type PRReviewUiTools = InferUITools; +export type PRReviewUiMessage = UIMessage; +// #endregion diff --git a/references/ai-chat/src/lib/prisma.ts b/references/ai-chat/src/lib/prisma.ts new file mode 100644 index 00000000000..5e78334aa82 --- /dev/null +++ b/references/ai-chat/src/lib/prisma.ts @@ -0,0 +1,15 @@ +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../../lib/generated/prisma/client"; + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }; + +function createClient() { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + return new PrismaClient({ adapter }); +} + +export const prisma = globalForPrisma.prisma ?? createClient(); + +if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = prisma; +} diff --git a/references/ai-chat/src/trigger/chat-client-test.ts b/references/ai-chat/src/trigger/chat-client-test.ts new file mode 100644 index 00000000000..742246eca2a --- /dev/null +++ b/references/ai-chat/src/trigger/chat-client-test.ts @@ -0,0 +1,449 @@ +/** + * Test tasks demonstrating the AgentChat and ChatStream APIs + * for server-side agent interaction. + */ +import { task, logger } from "@trigger.dev/sdk"; +import { chat } from "@trigger.dev/sdk/ai"; +import { AgentChat, ChatStream } from "@trigger.dev/sdk/chat"; +import type { aiChat, upgradeTestAgent } from "./chat"; +import type { prReviewChat } from "./pr-review"; + +// ─── Example 1: Simple multi-turn conversation ───────────────────── + +export const chatClientTest = task({ + id: "chat-client-test", + run: async (payload: { message: string; followUp?: string }) => { + const chat = new AgentChat({ + agent: "ai-chat", + clientData: { userId: "chat-client-test", model: "gpt-4o-mini" }, + }); + + await chat.preload(); + + // Send and get text back + const text = await (await chat.sendMessage(payload.message)).text(); + logger.info("Response", { preview: text.slice(0, 200) }); + + // Follow-up reuses the same run + if (payload.followUp) { + const { text: followUp, toolCalls } = await (await chat.sendMessage(payload.followUp)).result(); + logger.info("Follow-up", { + preview: followUp.slice(0, 200), + toolCalls: toolCalls.map((tc) => tc.toolName), + }); + } + + await chat.close(); + return { chatId: chat.id, text: text.slice(0, 500) }; + }, +}); + +// ─── Example 2: Streaming chunks ─────────────────────────────────── + +export const streamingTest = task({ + id: "chat-client-streaming-test", + run: async (payload: { message: string }) => { + const chat = new AgentChat({ + agent: "ai-chat", + clientData: { userId: "streaming-test", model: "gpt-4o-mini" }, + }); + + await chat.preload(); + + const stream = await chat.sendMessage(payload.message); + + let charCount = 0; + const toolsUsed: string[] = []; + + for await (const chunk of stream) { + if (chunk.type === "text-delta") { + charCount += chunk.delta.length; + } + if (chunk.type === "tool-input-available") { + toolsUsed.push(chunk.toolName); + logger.info("Agent using tool", { tool: chunk.toolName, input: chunk.input }); + } + if (chunk.type === "tool-output-available") { + logger.info("Tool output", { toolCallId: chunk.toolCallId }); + } + } + + await chat.close(); + return { charCount, toolsUsed }; + }, +}); + +// ─── Example 3: PR review agent (typed clientData) ───────────────── + +export const prReviewTest = task({ + id: "chat-client-pr-review-test", + run: async (payload: { prNumber: number }) => { + const chat = new AgentChat({ + agent: "pr-review", + id: `pr-review-${payload.prNumber}`, + clientData: { + userId: "ci-bot", + githubUrl: "https://github.com/ericallam/definitely-safe-ai", + }, + }); + + await chat.preload(); + + const review = await (await chat.sendMessage(`Review PR #${payload.prNumber}`)).result(); + logger.info("Review complete", { + textLength: review.text.length, + toolCalls: review.toolCalls.map((tc) => `${tc.toolName}(${JSON.stringify(tc.input)})`), + }); + + const fix = await ( + await chat.sendMessage("Can you suggest a fix for the most critical issue and verify it works?") + ).result(); + + await chat.close(); + + return { + reviewPreview: review.text.slice(0, 500), + fixPreview: fix.text.slice(0, 500), + toolsUsed: [ + ...review.toolCalls.map((tc) => tc.toolName), + ...fix.toolCalls.map((tc) => tc.toolName), + ], + }; + }, +}); + +// ─── Example 4: Low-level sendRaw + ChatStream ───────────────────── + +export const lowLevelTest = task({ + id: "chat-client-low-level-test", + run: async (payload: { message: string }) => { + const chat = new AgentChat({ + agent: "ai-chat", + clientData: { userId: "low-level-test", model: "gpt-4o-mini" }, + }); + + await chat.preload(); + + // sendRaw for full control over the UIMessage shape + const rawStream = await chat.sendRaw([ + { + id: `msg-${Date.now()}`, + role: "user", + parts: [{ type: "text", text: payload.message }], + }, + ]); + + const stream = new ChatStream(rawStream); + const { text, toolCalls } = await stream.result(); + + await chat.close(); + return { text: text.slice(0, 500), toolCalls: toolCalls.map((tc) => tc.toolName) }; + }, +}); + +// ─── Example 5: Agent-to-agent orchestration ─────────────────────── + +export const orchestratorTest = task({ + id: "chat-client-orchestrator-test", + run: async (payload: { topic: string }) => { + const researcher = new AgentChat({ + agent: "ai-chat", + clientData: { userId: "orchestrator", model: "gpt-4o-mini" }, + }); + + await researcher.preload(); + + const research = await ( + await researcher.sendMessage(`Research this topic and summarize key findings: ${payload.topic}`) + ).text(); + + const analysis = await ( + await researcher.sendMessage( + "Based on your research, what are the top 3 actionable recommendations?" + ) + ).text(); + + await researcher.close(); + + return { + research: research.slice(0, 500), + analysis: analysis.slice(0, 500), + }; + }, +}); + +// ─── Example 6: Single-turn sub-agent tool ───────────────────────── + +import { tool as aiTool, streamText, stepCountIs } from "ai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { z } from "zod"; + +const prReviewTool = aiTool({ + description: "Delegate a PR review to the PR review agent.", + inputSchema: z.object({ + prNumber: z.number().describe("The PR number to review"), + repo: z.string().describe("The GitHub repo URL"), + }), + execute: async function* ({ prNumber, repo }, { abortSignal }) { + const chat = new AgentChat({ + agent: "pr-review", + id: `sub-review-${prNumber}`, + clientData: { userId: "parent-agent", githubUrl: repo }, + }); + + await chat.preload(); + const stream = await chat.sendMessage(`Review PR #${prNumber}`, { abortSignal }); + yield* stream.messages(); + await chat.close(); + }, + toModelOutput: ({ output: message }) => { + const lastText = message?.parts?.findLast( + (p: { type: string }) => p.type === "text" + ) as { text?: string } | undefined; + return { type: "text" as const, value: lastText?.text ?? "Review complete." }; + }, +}); + +// ─── Example 7: Multi-turn sub-agent (LLM-driven, cross-turn) ────── + +export const orchestratorAgent = chat + .withClientData({ + schema: z.object({ userId: z.string() }), + }) + .customAgent({ + id: "orchestrator-agent", + run: async (payload, { signal: runSignal }) => { + let currentPayload: typeof payload = payload; + + // Sub-agent instances live in the run closure — survive across turns + const subAgents = new Map>(); + + const researchAgentTool = aiTool({ + description: + "Talk to a research agent. Use the same conversationId to continue " + + "an existing conversation — the agent remembers full context.", + inputSchema: z.object({ + conversationId: z.string().describe("Reuse to continue a conversation."), + message: z.string().describe("Your message to the research agent"), + }), + execute: async function* ({ conversationId, message }, { abortSignal }) { + let agent = subAgents.get(conversationId); + if (!agent) { + agent = new AgentChat({ + agent: "ai-chat", + id: conversationId, + clientData: { + userId: currentPayload.metadata?.userId ?? "orchestrator", + model: "gpt-4o-mini", + }, + }); + await agent.preload(); + subAgents.set(conversationId, agent); + } + + const stream = await agent.sendMessage(message, { abortSignal }); + yield* stream.messages(); + }, + toModelOutput: ({ output: message }) => { + const lastText = message?.parts?.findLast( + (p: { type: string }) => p.type === "text" + ) as { text?: string } | undefined; + return { type: "text" as const, value: lastText?.text ?? "Research complete." }; + }, + }); + + // Preload handling + if (currentPayload.trigger === "preload") { + const result = await chat.messages.waitWithIdleTimeout({ + idleTimeoutInSeconds: 120, + timeout: "1h", + spanName: "waiting for first message", + }); + if (!result.ok) return; + currentPayload = result.output as typeof payload; + } + + if (currentPayload.trigger === "close") return; + + const stop = chat.createStopSignal(); + const conversation = new chat.MessageAccumulator(); + + for (let turn = 0; turn < 50; turn++) { + stop.reset(); + + const messages = await conversation.addIncoming( + currentPayload.messages, + currentPayload.trigger, + turn + ); + + const combinedSignal = AbortSignal.any([runSignal, stop.signal]); + + const result = streamText({ + model: anthropic("claude-sonnet-4-6"), + system: + "You are an orchestrator that delegates research to a sub-agent. " + + "Use the researchAgent tool with a conversationId to start or continue " + + "a research thread.", + messages, + tools: { researchAgent: researchAgentTool }, + stopWhen: stepCountIs(15), + abortSignal: combinedSignal, + }); + + let response; + try { + response = await chat.pipeAndCapture(result, { signal: combinedSignal }); + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + if (runSignal.aborted) break; + } else { + throw error; + } + } + + if (response) { + if (stop.signal.aborted && !runSignal.aborted) { + await conversation.addResponse(chat.cleanupAbortedParts(response)); + } else { + await conversation.addResponse(response); + } + } + + if (runSignal.aborted) break; + + await chat.writeTurnComplete(); + + const next = await chat.messages.waitWithIdleTimeout({ + idleTimeoutInSeconds: 120, + timeout: "1h", + spanName: "waiting for next message", + }); + if (!next.ok) break; + currentPayload = next.output as typeof payload; + if (currentPayload.trigger === "close") break; + } + + // Cleanup sub-agents + const closePromises = Array.from(subAgents.values()).map((a) => + a.close().catch(() => { }) + ); + await Promise.all(closePromises); + + stop.cleanup(); + }, + }); + +// ─── Example 8: chat.requestUpgrade() test ──────────────────────── + +export const upgradeTest = task({ + id: "chat-client-upgrade-test", + run: async () => { + const agentChat = new AgentChat({ + agent: "upgrade-test", + }); + + const results: { turn: number; text: string; runId?: string }[] = []; + + // Send 6 messages — the agent requests an upgrade after turn 3 (0-indexed), + // so the run exits after the 4th response. The 5th message triggers a + // continuation on a new run, and the 6th message continues on that run. + for (let i = 0; i < 6; i++) { + const stream = await agentChat.sendMessage(`This is message ${i + 1}. What turn are you on?`); + const text = await stream.text(); + + // If we get an empty response, the run just exited — wait a moment + // for it to fully complete, then retry (triggers continuation) + if (text === "" && i > 0) { + logger.info(`Turn ${i}: empty response, retrying after run completes`); + await new Promise((r) => setTimeout(r, 2000)); + const retryStream = await agentChat.sendMessage( + `This is message ${i + 1} (retry). What turn are you on?` + ); + const retryText = await retryStream.text(); + results.push({ + turn: i, + text: retryText.slice(0, 200), + runId: agentChat.id, + }); + logger.info(`Turn ${i} (retry)`, { + text: retryText.slice(0, 200), + runId: agentChat.id, + }); + continue; + } + + results.push({ + turn: i, + text: text.slice(0, 200), + runId: agentChat.id, + }); + logger.info(`Turn ${i}`, { text: text.slice(0, 200), runId: agentChat.id }); + } + + await agentChat.close(); + + // Check that a continuation happened — runId should change + const runIds = [...new Set(results.map((r) => r.runId))]; + logger.info("Upgrade test complete", { + totalTurns: results.length, + uniqueRuns: runIds.length, + runIds, + }); + + return { + turns: results, + uniqueRuns: runIds.length, + upgraded: runIds.length > 1, + }; + }, +}); + +// ─── Example 9: Quick-fire burst test ────────────────────────────── +// +// Fire N messages back-to-back without awaiting between sends. The agent's +// session.in queues records in arrival order; the per-turn loop processes +// the first one, and the rest land as pendingMessages mid-stream (or queue +// up for the next turn). Validates: (1) no records dropped at the dedup +// cutoff, (2) ordering preserved, (3) no race between snapshot.write of +// turn N and boot of turn N+1, (4) all responses eventually arrive. + +export const burstTest = task({ + id: "chat-client-burst-test", + run: async (payload: { count?: number }) => { + const count = payload.count ?? 5; + const agentChat = new AgentChat({ + agent: "ai-chat", + clientData: { userId: "burst-test", model: "gpt-4o-mini" }, + }); + await agentChat.preload(); + + const start = Date.now(); + + // Fire all N concurrently. Each call POSTs to /in/append immediately; + // the agent dequeues in arrival order and processes sequentially. + const sends = Array.from({ length: count }, (_, i) => + agentChat + .sendMessage(`Reply with the single word: msg-${i + 1}.`) + .then((stream) => stream.text()) + .then((text) => ({ idx: i + 1, text: text.slice(0, 80), error: null as string | null })) + .catch((err) => ({ idx: i + 1, text: "", error: String(err?.message || err) })) + ); + + const results = await Promise.all(sends); + const elapsedMs = Date.now() - start; + + await agentChat.close(); + + return { + chatId: agentChat.id, + count, + elapsedMs, + results, + anyErrors: results.filter((r) => r.error).length, + orderedTextsContainingIndex: results.map((r) => + r.text.toLowerCase().includes(`msg-${r.idx}`) ? "ok" : "miss" + ), + }; + }, +}); diff --git a/references/ai-chat/src/trigger/chat-tools.ts b/references/ai-chat/src/trigger/chat-tools.ts new file mode 100644 index 00000000000..dfb7e89e97e --- /dev/null +++ b/references/ai-chat/src/trigger/chat-tools.ts @@ -0,0 +1,402 @@ +/** + * Tool executes for the trigger.dev agent task. + * + * These tools wrap the schema-only definitions from + * `@/lib/chat-tools-schemas` with their heavy `execute` fns. This + * file is ONLY imported from inside the trigger task module + * (`src/trigger/chat.ts`); it must NOT be imported from anything that + * runs in the Next.js process (route handlers, components, server + * actions, etc.). + * + * See `src/lib/chat-tools-schemas.ts` for why this split matters — + * the bundle-isolation constraint is what makes `chat.handover`'s + * cold-start win possible. + */ +import { ai, chat } from "@trigger.dev/sdk/ai"; +import { schemaTask } from "@trigger.dev/sdk"; +import { tool, generateId } from "ai"; +import { z } from "zod"; +import os from "node:os"; +import TurndownService from "turndown"; +import { codeSandboxRun, runWithCodeSandbox } from "@/lib/code-sandbox"; +import { + inspectEnvironment as inspectEnvironmentSchema, + webFetch as webFetchSchema, + deepResearch as deepResearchSchema, + posthogQuery as posthogQuerySchema, + executeCode as executeCodeSchema, + sendEmail as sendEmailSchema, + askUser as askUserSchema, + getCurrentTime as getCurrentTimeSchema, + searchHackerNews as searchHackerNewsSchema, + createGithubIssue as createGithubIssueSchema, +} from "@/lib/chat-tools-schemas"; + +const turndown = new TurndownService(); + +declare const Bun: unknown; +declare const Deno: unknown; + +export const inspectEnvironment = tool({ + ...inspectEnvironmentSchema, + execute: async () => { + const memUsage = process.memoryUsage(); + return { + runtime: { + name: typeof Bun !== "undefined" ? "bun" : typeof Deno !== "undefined" ? "deno" : "node", + version: process.version, + versions: { + v8: process.versions.v8, + openssl: process.versions.openssl, + modules: process.versions.modules, + }, + }, + os: { + platform: process.platform, + arch: process.arch, + release: os.release(), + type: os.type(), + hostname: os.hostname(), + uptime: `${Math.floor(os.uptime())}s`, + }, + cpus: { + count: os.cpus().length, + model: os.cpus()[0]?.model, + }, + memory: { + total: `${Math.round(os.totalmem() / 1024 / 1024)}MB`, + free: `${Math.round(os.freemem() / 1024 / 1024)}MB`, + process: { + rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`, + heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, + heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, + }, + }, + env: { + NODE_ENV: process.env.NODE_ENV, + TZ: process.env.TZ ?? Intl.DateTimeFormat().resolvedOptions().timeZone, + LANG: process.env.LANG, + }, + process: { + pid: process.pid, + cwd: process.cwd(), + execPath: process.execPath, + argv: process.argv.slice(0, 3), + }, + }; + }, +}); + +export const webFetch = tool({ + ...webFetchSchema, + execute: async ({ url }) => { + const latency = Number(process.env.WEBFETCH_LATENCY_MS); + if (latency > 0) { + await new Promise((r) => setTimeout(r, latency)); + } + + const response = await fetch(url); + let text = await response.text(); + const contentType = response.headers.get("content-type") ?? ""; + + if (contentType.includes("html")) { + text = turndown.turndown(text); + } + + return { + status: response.status, + contentType, + body: text.slice(0, 2000), + truncated: text.length > 2000, + }; + }, +}); + +const deepResearchTask = schemaTask({ + id: "deep-research", + description: + "Research a topic by fetching multiple URLs and synthesizing the results. " + + "Streams progress updates to the chat as it works.", + schema: z.object({ + query: z.string().describe("The research query or topic"), + urls: z.array(z.string().url()).describe("URLs to fetch and analyze"), + }), + run: async ({ query, urls }) => { + const partId = generateId(); + const results: { url: string; status: number; snippet: string }[] = []; + + for (let i = 0; i < urls.length; i++) { + const url = urls[i]!; + + const { waitUntilComplete } = chat.stream.writer({ + target: "root", + execute: ({ write }) => { + write({ + type: "data-research-progress", + id: partId, + data: { + status: "fetching" as const, + query, + current: i + 1, + total: urls.length, + currentUrl: url, + completedUrls: results.map((r) => r.url), + }, + }); + }, + }); + await waitUntilComplete(); + + try { + const response = await fetch(url); + let text = await response.text(); + const contentType = response.headers.get("content-type") ?? ""; + + if (contentType.includes("html")) { + text = turndown.turndown(text); + } + + results.push({ + url, + status: response.status, + snippet: text.slice(0, 500), + }); + } catch (err) { + results.push({ + url, + status: 0, + snippet: `Error: ${err instanceof Error ? err.message : String(err)}`, + }); + } + } + + const { waitUntilComplete: waitForDone } = chat.stream.writer({ + target: "root", + execute: ({ write }) => { + write({ + type: "data-research-progress", + id: partId, + data: { + status: "done" as const, + query, + current: urls.length, + total: urls.length, + completedUrls: results.map((r) => r.url), + }, + }); + }, + }); + await waitForDone(); + + return { query, results }; + }, +}); + +/** Task-backed tool: AI SDK `tool()` for shape/types; `ai.toolExecute` for Trigger subtask + metadata. */ +export const deepResearch = tool({ + ...deepResearchSchema, + execute: ai.toolExecute(deepResearchTask), +}); + +const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY; +const POSTHOG_PROJECT_ID = process.env.POSTHOG_PROJECT_ID; +const POSTHOG_HOST = process.env.POSTHOG_HOST ?? "https://eu.posthog.com"; + +export const posthogQuery = tool({ + ...posthogQuerySchema, + execute: async ({ query }) => { + if (!POSTHOG_API_KEY || !POSTHOG_PROJECT_ID) { + return { error: "PostHog not configured. Set POSTHOG_API_KEY and POSTHOG_PROJECT_ID." }; + } + const response = await fetch(`${POSTHOG_HOST}/api/projects/${POSTHOG_PROJECT_ID}/query/`, { + method: "POST", + headers: { + Authorization: `Bearer ${POSTHOG_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ query: { kind: "HogQLQuery", query } }), + }); + + if (!response.ok) { + const text = await response.text(); + return { error: `PostHog API error ${response.status}: ${text.slice(0, 500)}` }; + } + + const data = await response.json(); + return { + columns: data.columns, + results: data.results?.slice(0, 50), + rowCount: data.results?.length ?? 0, + }; + }, +}); + +export const executeCode = tool({ + ...executeCodeSchema, + execute: async function executeCodeExecute({ code, language }) { + const runId = codeSandboxRun.runId; + if (!runId?.trim()) { + return { + error: + "Code sandbox run id is not set yet (call from the chat task after onTurnStart), or this tool is not wired to that task.", + }; + } + + const out = await runWithCodeSandbox(runId, async function runInSandbox(sandbox) { + const execution = await sandbox.runCode(code, { + ...(language?.trim() ? { language: language.trim() } : {}), + timeoutMs: 60_000, + }); + + if (execution.error) { + return { + error: `${execution.error.name}: ${execution.error.value}`, + traceback: execution.error.traceback, + stdout: execution.logs.stdout.join("\n"), + stderr: execution.logs.stderr.join("\n"), + }; + } + + const mainText = execution.text; + const resultSnippets = execution.results + .map(function mapResult(r) { + return r.text ?? r.markdown ?? r.json; + }) + .filter(Boolean) + .slice(0, 5); + + return { + text: mainText, + results: resultSnippets, + stdout: execution.logs.stdout.join("\n"), + stderr: execution.logs.stderr.join("\n"), + }; + }); + + return out; + }, +}); + +export const sendEmail = tool({ + ...sendEmailSchema, + execute: async ({ to, subject, body }) => { + // Simulated — in a real app this would call an email API + return { sent: true, to, subject, preview: body.slice(0, 100) }; + }, +}); + +// askUser has no execute by design — round-tripped via addToolOutput. +export const askUser = askUserSchema; + +export const getCurrentTime = tool({ + ...getCurrentTimeSchema, + execute: async () => { + const now = new Date(); + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + return { + iso: now.toISOString(), + unixMs: now.getTime(), + timezone: tz, + local: now.toLocaleString("en-US", { timeZone: tz, dateStyle: "full", timeStyle: "long" }), + utc: now.toUTCString(), + dayOfWeek: now.toLocaleDateString("en-US", { weekday: "long" }), + }; + }, +}); + +export const searchHackerNews = tool({ + ...searchHackerNewsSchema, + execute: async ({ query, limit = 5 }) => { + if (query) { + // Algolia HN search — story type only, sorted by points + const url = new URL("https://hn.algolia.com/api/v1/search"); + url.searchParams.set("query", query); + url.searchParams.set("tags", "story"); + url.searchParams.set("hitsPerPage", String(limit)); + const res = await fetch(url); + if (!res.ok) return { error: `Algolia error ${res.status}` }; + const json = (await res.json()) as { + hits: Array<{ + objectID: string; + title?: string; + url?: string; + author: string; + points?: number; + num_comments?: number; + created_at: string; + }>; + }; + return { + query, + results: json.hits.map((h) => ({ + title: h.title ?? "(no title)", + url: h.url ?? `https://news.ycombinator.com/item?id=${h.objectID}`, + author: h.author, + points: h.points ?? 0, + comments: h.num_comments ?? 0, + createdAt: h.created_at, + })), + }; + } + // Top stories — first /topstories.json then per-item lookups + const idsRes = await fetch("https://hacker-news.firebaseio.com/v0/topstories.json"); + if (!idsRes.ok) return { error: `HN error ${idsRes.status}` }; + const ids = (await idsRes.json()) as number[]; + const top = ids.slice(0, limit); + const items = await Promise.all( + top.map(async (id) => { + const r = await fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`); + if (!r.ok) return null; + const it = (await r.json()) as { + id: number; + title?: string; + url?: string; + by: string; + score?: number; + descendants?: number; + time: number; + }; + return { + title: it.title ?? "(no title)", + url: it.url ?? `https://news.ycombinator.com/item?id=${it.id}`, + author: it.by, + points: it.score ?? 0, + comments: it.descendants ?? 0, + createdAt: new Date(it.time * 1000).toISOString(), + }; + }) + ); + return { topStories: items.filter((x) => x !== null) }; + }, +}); + +export const createGithubIssue = tool({ + ...createGithubIssueSchema, + execute: async ({ repo, title, body, labels }) => { + // Simulated — in a real app this would call the GitHub API + const issueNumber = Math.floor(Math.random() * 9000) + 1000; + return { + created: true, + repo, + issueNumber, + url: `https://github.com/${repo}/issues/${issueNumber}`, + title, + labels: labels ?? [], + preview: body.slice(0, 120), + }; + }, +}); + +/** Tool set passed to `streamText` for the main `chat.agent` run. */ +export const chatTools = { + inspectEnvironment, + webFetch, + deepResearch, + posthogQuery, + executeCode, + sendEmail, + askUser, + getCurrentTime, + searchHackerNews, + createGithubIssue, +}; diff --git a/references/ai-chat/src/trigger/chat.ts b/references/ai-chat/src/trigger/chat.ts new file mode 100644 index 00000000000..2d90ca6dfef --- /dev/null +++ b/references/ai-chat/src/trigger/chat.ts @@ -0,0 +1,1005 @@ +import { chat, type ChatTaskWirePayload } from "@trigger.dev/sdk/ai"; +import { logger, prompts, skills } from "@trigger.dev/sdk"; + +import { + streamText, + generateText, + generateObject, + stepCountIs, + generateId, + createProviderRegistry, + validateUIMessages, +} from "ai"; +import type { LanguageModel, LanguageModelUsage, UIMessage } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { z } from "zod"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../../lib/generated/prisma/client"; +import { + chatTools, + deepResearch, + inspectEnvironment, + webFetch, +} from "./chat-tools"; +import type { ChatUiMessage } from "@/lib/chat-tools-schemas"; +import { disposeCodeSandboxForRun, warmCodeSandbox } from "@/lib/code-sandbox"; + +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); +const prisma = new PrismaClient({ adapter }); + +/** Prisma `messages` JSON column — use write-side type for updates (not `JsonValue` from reads). */ +export type ChatMessagesForWrite = NonNullable< + Parameters[0]["data"] +>["messages"]; + +import { DEFAULT_MODEL, REASONING_MODELS } from "@/lib/models"; + +function textFromFirstPart(message: UIMessage): string { + const p = message.parts?.[0]; + return p?.type === "text" ? p.text : ""; +} +const COMPACT_AFTER_TOKENS = Number(process.env.COMPACT_AFTER_TOKENS) || 80_000; + +const registry = createProviderRegistry({ openai, anthropic }); + +type RegistryLanguageModelId = Parameters[0]; + +function registryLanguageModel( + id: string | undefined, + fallback: RegistryLanguageModelId +): LanguageModel { + return registry.languageModel((id ?? fallback) as RegistryLanguageModelId); +} + +// #region Managed prompts — versioned, overridable from dashboard +const compactionPrompt = prompts.define({ + id: "ai-chat-compaction", + model: "openai:gpt-4o-mini" satisfies RegistryLanguageModelId, + content: `You are a conversation compactor. You will receive a transcript of a multi-turn conversation between a user and an assistant. + +Produce a concise summary that captures: +- The topics discussed and questions asked +- Any key facts, answers, or decisions reached +- Important context needed to continue the conversation naturally + +Write in third person (e.g. "The user asked about..." / "The assistant explained..."). +Keep it under 300 words. Do not include greetings or filler.`, +}); + +const systemPrompt = prompts.define({ + id: "ai-chat-system", + model: "openai:gpt-4o" satisfies RegistryLanguageModelId, + config: { temperature: 0.7 }, + variables: z.object({ name: z.string(), plan: z.string() }), + content: `You are a helpful AI assistant for {{name}} on the {{plan}} plan. + +## Guidelines +- Be concise and friendly. Prefer short, direct answers unless the user asks for detail. +- When using tools, explain what you're doing briefly before invoking them. +- If you don't know something, say so — don't make things up. + +## Capabilities +You can inspect the execution environment, fetch web pages, perform multi-URL deep research, +query PostHog with HogQL, and run short code snippets in an isolated sandbox (e.g. to analyze query results). +When the user asks you to research a topic, use the deep research tool with relevant URLs. + +## Tone +- Match the user's formality level. If they're casual, be casual back. +- Use markdown formatting for code blocks, lists, and structured output. +- Keep responses under a few paragraphs unless the user asks for more.`, +}); + +const timeUtilsSkill = skills.define({ + id: "time-utils", + path: "./skills/time-utils", +}); + +const selfReviewPrompt = prompts.define({ + id: "ai-chat-self-review", + model: "openai:gpt-4o-mini" satisfies RegistryLanguageModelId, + content: `You are a conversation quality reviewer. Analyze the assistant's most recent response and provide structured feedback. + +Focus on: +- Whether the response actually answered the user's question +- Missed opportunities to use tools or provide more detail +- Tone mismatches (too formal, too casual, etc.) +- Factual claims that should have been verified with tools + +Be concise. Only flag issues worth fixing — don't nitpick.`, +}); +// #endregion + +// #region Models and helpers +const MODELS: Record LanguageModel> = { + "gpt-4o-mini": () => openai("gpt-4o-mini"), + "gpt-4o": () => openai("gpt-4o"), + "claude-sonnet-4-6": () => anthropic("claude-sonnet-4-6"), + "claude-opus-4-6": () => anthropic("claude-opus-4-6"), +}; + +function getModel(modelId?: string): LanguageModel { + const factory = MODELS[modelId ?? DEFAULT_MODEL]; + if (!factory) return MODELS[DEFAULT_MODEL]!(); + return factory(); +} + +const DEFAULT_REGISTRY_MODEL_ID = "anthropic:claude-sonnet-4-6" as const satisfies RegistryLanguageModelId; + +function languageModelForChatTurn(modelOverride: string | null | undefined): LanguageModel { + if (modelOverride) { + return getModel(modelOverride); + } + return registryLanguageModel(chat.prompt().model, DEFAULT_REGISTRY_MODEL_ID); +} + +function useExtendedThinking(modelOverride: string | null | undefined): boolean { + if (modelOverride && REASONING_MODELS.has(modelOverride)) { + return true; + } + const promptModel = chat.prompt().model; + return promptModel != null && promptModel.includes("claude-opus-4-6"); +} +// #endregion + +// #region Per-run state — chat.local persists across turns in the same run +const userContext = chat.local<{ + userId: string; + name: string; + plan: "free" | "pro"; + preferredModel: string | null; + messageCount: number; +}>({ id: "userContext" }); +// #endregion + +// ============================================================================ +// chat.agent — the main chat agent +// ============================================================================ + +export const aiChat = chat + .withUIMessage({ + streamOptions: { + sendReasoning: true, + onError: (error) => { + logger.error("Stream error", { error }); + if (error instanceof Error && error.message.includes("rate limit")) { + return "Rate limited — please wait a moment and try again."; + } + return "Something went wrong. Please try again."; + }, + }, + }) + .withClientData({ + schema: z.object({ model: z.string().optional(), userId: z.string() }), + }) + .onChatSuspend(async ({ phase, ctx }) => { + logger.debug("Chat suspending", { phase, runId: ctx.run.id }); + await disposeCodeSandboxForRun(ctx.run.id); + }) + .onChatResume(async ({ phase, ctx }) => { + logger.debug("Chat resumed", { phase, runId: ctx.run.id }); + }) + .agent({ + id: "ai-chat", + idleTimeoutInSeconds: 60, + chatAccessTokenTTL: "1h", + + // #region Compaction — automatic context window management + compaction: { + shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > COMPACT_AFTER_TOKENS, + summarize: async ({ messages }) => { + const resolved = await compactionPrompt.resolve({}); + return generateText({ + model: registryLanguageModel(resolved.model, "openai:gpt-4o-mini"), + messages: [...messages, { role: "user" as const, content: resolved.text }], + ...resolved.toAISDKTelemetry(), + }).then((r) => r.text); + }, + compactUIMessages: ({ uiMessages, summary }) => { + return [ + { + id: generateId(), + role: "assistant" as const, + parts: [{ type: "text" as const, text: `[Conversation summary]\n\n${summary}` }], + }, + ...uiMessages.slice(-2), + ]; + }, + }, + // #endregion + + // #region Pending messages — user can steer the agent mid-response + pendingMessages: { + shouldInject: ({ steps }) => steps.length > 0, + prepare: ({ messages }) => + messages.length === 1 + ? [{ role: "user" as const, content: textFromFirstPart(messages[0]!) }] + : [ + { + role: "user" as const, + content: `The user sent ${messages.length + } messages while you were working:\n\n${messages + .map((m, i) => `${i + 1}. ${textFromFirstPart(m)}`) + .join("\n")}`, + }, + ], + }, + // #endregion + + // #region onValidateMessages — validate UIMessages before model conversion + onValidateMessages: async ({ messages, turn }) => { + logger.info("Validating UI messages", { + turn, + count: messages.length, + }); + // Cast: `chatTools` has executes (output types are real), but + // `ChatUiMessage` is derived from the schema-only set in + // `chat-tools-schemas.ts` so its tools have `output: never`. + // `validateUIMessages` only reads `inputSchema` at runtime, so + // the type narrowing is safely sidestepped. + return validateUIMessages({ + messages, + tools: chatTools as unknown as Parameters[0]["tools"], + }); + }, + // #endregion + + // #region prepareMessages — runs before every LLM call + prepareMessages: ({ messages, reason }) => { + // Add Anthropic cache breaks to the last message for prompt caching. + if (messages.length === 0) return messages; + const last = messages[messages.length - 1]!; + return [ + ...messages.slice(0, -1), + { + ...last, + providerOptions: { + ...last.providerOptions, + anthropic: { + ...(last.providerOptions?.anthropic as Record | undefined), + cacheControl: { type: "ephemeral" }, + }, + }, + }, + ]; + }, + // #endregion + + // --- Lifecycle hooks --- + + // #region onPreload — eagerly initialize before the user's first message + onPreload: async ({ chatId, runId, chatAccessToken, clientData }) => { + if (!clientData) return; + const user = await prisma.user.upsert({ + where: { id: clientData.userId }, + create: { id: clientData.userId, name: "User" }, + update: {}, + }); + userContext.init({ + userId: user.id, + name: user.name, + plan: user.plan as "free" | "pro", + preferredModel: user.preferredModel, + messageCount: user.messageCount, + }); + + const resolved = await systemPrompt.resolve({ + name: user.name, + plan: user.plan as string, + }); + chat.prompt.set(resolved); + chat.skills.set([await timeUtilsSkill.local()]); + + await prisma.chat.upsert({ + where: { id: chatId }, + create: { + id: chatId, + title: "New chat", + userId: user.id, + model: clientData?.model ?? DEFAULT_MODEL, + }, + update: {}, + }); + await prisma.chatSession.upsert({ + where: { id: chatId }, + create: { id: chatId, publicAccessToken: chatAccessToken }, + update: { publicAccessToken: chatAccessToken }, + }); + }, + // #endregion + + // #region onChatStart — fallback init when not preloaded + onChatStart: async ({ + chatId, + runId, + chatAccessToken, + clientData, + continuation, + preloaded, + }) => { + if (preloaded) return; + + const user = await prisma.user.upsert({ + where: { id: clientData.userId }, + create: { id: clientData.userId, name: "User" }, + update: {}, + }); + userContext.init({ + userId: user.id, + name: user.name, + plan: user.plan as "free" | "pro", + preferredModel: user.preferredModel, + messageCount: user.messageCount, + }); + + const resolved = await systemPrompt.resolve({ + name: user.name, + plan: user.plan as string, + }); + chat.prompt.set(resolved); + chat.skills.set([await timeUtilsSkill.local()]); + + if (!continuation) { + await prisma.chat.upsert({ + where: { id: chatId }, + create: { + id: chatId, + title: "New chat", + userId: user.id, + model: clientData.model ?? DEFAULT_MODEL, + }, + update: {}, + }); + } + + await prisma.chatSession.upsert({ + where: { id: chatId }, + create: { id: chatId, publicAccessToken: chatAccessToken }, + update: { publicAccessToken: chatAccessToken }, + }); + }, + // #endregion + + // #region onCompacted + onCompacted: async ({ summary, totalTokens, messageCount, chatId, turn }) => { + logger.info("Conversation compacted", { + chatId, + turn, + totalTokens, + messageCount, + summaryLength: summary.length, + }); + }, + // #endregion + + // #region onTurnStart — persist messages + write status via writer + onTurnStart: async ({ chatId, uiMessages, writer, runId }) => { + warmCodeSandbox(runId); + writer.write({ type: "data-turn-status", data: { status: "preparing" }, transient: true }); + // Awaited (not chat.defer) so the user message is durable before + // streaming begins. A mid-stream page refresh reads from DB; if the + // write is still in flight, getChatMessages returns [] and the + // resumed SSE stream rebuilds an assistant-only conversation, + // dropping the user message from the UI. + await prisma.chat.update({ + where: { id: chatId }, + data: { messages: uiMessages as unknown as ChatMessagesForWrite }, + }); + }, + // #endregion + + onComplete: async ({ ctx }) => { + await disposeCodeSandboxForRun(ctx.run.id); + }, + + // #region onBeforeTurnComplete — add a persistent data part to test chat.response + onBeforeTurnComplete: async ({ writer, turn }) => { + writer.write({ + type: "data-turn-metadata", + data: { turn, timestamp: Date.now(), source: "onBeforeTurnComplete" }, + }); + }, + // #endregion + + // #region actionSchema + onAction — typed actions for state-only mutations + // Actions are not turns: only `hydrateMessages` and `onAction` fire, + // no `run()` invocation, no model call. The `undo` action drops the + // last user/assistant exchange so the next message turn sees a + // truncated history. + actionSchema: z.discriminatedUnion("type", [ + z.object({ type: z.literal("undo") }), + ]), + onAction: async ({ action }) => { + if (action.type === "undo") { + chat.history.slice(0, -2); + } + }, + // #endregion + + // #region onTurnComplete — persist + background self-review via chat.inject() + onTurnComplete: async ({ + chatId, + uiMessages, + messages, + responseMessage, + runId, + chatAccessToken, + lastEventId, + }) => { + // Log responseMessage parts for debugging TRI-8556 + const partTypes = responseMessage?.parts?.map((p: any) => p.type) ?? []; + const toolParts = responseMessage?.parts?.filter((p: any) => p.type?.startsWith("tool-")) ?? []; + logger.info("onTurnComplete responseMessage", { + hasResponseMessage: !!responseMessage, + responseMessageId: responseMessage?.id, + totalParts: responseMessage?.parts?.length ?? 0, + partTypes, + toolPartsCount: toolParts.length, + toolParts: toolParts.map((p: any) => ({ type: p.type, state: p.state, toolCallId: p.toolCallId })), + }); + // Atomic so the page-load `Promise.all([getChatMessages, getSessionForChat])` + // can't observe a state where messages are post-write but lastEventId is + // still pre-write — that race causes resume to replay this turn's chunks + // on top of the persisted assistant message and duplicates the render. + await prisma.$transaction([ + prisma.chat.update({ + where: { id: chatId }, + data: { messages: uiMessages as unknown as ChatMessagesForWrite }, + }), + prisma.chatSession.upsert({ + where: { id: chatId }, + create: { id: chatId, publicAccessToken: chatAccessToken, lastEventId }, + update: { publicAccessToken: chatAccessToken, lastEventId }, + }), + ]); + + // Background self-review — a cheap model critiques the response and + // injects coaching into the conversation before the next user message. + chat.defer( + (async () => { + const resolved = await selfReviewPrompt.resolve({}); + + const review = await generateObject({ + model: registryLanguageModel(resolved.model, "openai:gpt-4o-mini"), + ...resolved.toAISDKTelemetry(), + system: resolved.text, + prompt: `Here is the conversation to review:\n\n${messages + .filter((m) => m.role === "user" || m.role === "assistant") + .map( + (m) => + `${m.role}: ${typeof m.content === "string" + ? m.content + : Array.isArray(m.content) + ? m.content + .filter((p: any) => p.type === "text") + .map((p: any) => p.text) + .join("") + : "" + }` + ) + .join("\n\n")}`, + schema: z.object({ + needsImprovement: z.boolean().describe("Whether the response needs improvement"), + suggestions: z + .array(z.string()) + .describe("Specific actionable suggestions for the next response"), + missedTools: z + .array(z.string()) + .describe("Tool names the assistant should have used but didn't"), + }), + }); + + const parts = []; + if (review.object.suggestions.length > 0) { + parts.push( + `Suggestions:\n${review.object.suggestions.map((s) => `- ${s}`).join("\n")}` + ); + } + if (review.object.missedTools.length > 0) { + parts.push(`Consider using: ${review.object.missedTools.join(", ")}`); + } + + chat.inject([ + { + role: "user" as const, + content: review.object.needsImprovement + ? `[Self-review of your previous response]\n\n${parts.join( + "\n\n" + )}\n\nApply these improvements naturally in your next response.` + : `[Self-review of your previous response]\n\nYour previous response was good. No changes needed.`, + }, + ]); + })() + ); + }, + // #endregion + + // #region run — just return streamText(), chat.agent handles everything else + run: async ({ messages, clientData, stopSignal }) => { + userContext.messageCount++; + if (clientData?.model) { + userContext.preferredModel = clientData.model; + } + + const modelOverride = clientData?.model ?? userContext.preferredModel ?? undefined; + const useReasoning = useExtendedThinking(modelOverride); + + return streamText({ + ...chat.toStreamTextOptions({ + registry, + telemetry: clientData?.userId ? { userId: clientData.userId } : undefined, + tools: chatTools, + }), + model: languageModelForChatTurn(modelOverride), + messages: messages, + stopWhen: stepCountIs(10), + abortSignal: stopSignal, + providerOptions: { + openai: { user: clientData?.userId }, + anthropic: { + metadata: { user_id: clientData?.userId }, + ...(useReasoning ? { thinking: { type: "enabled", budgetTokens: 10000 } } : {}), + }, + }, + }); + }, + // #endregion + }); + +// #region Raw task variant — same functionality using composable primitives +async function initUserContext(userId: string, chatId: string, model?: string) { + const user = await prisma.user.upsert({ + where: { id: userId }, + create: { id: userId, name: "User" }, + update: {}, + }); + userContext.init({ + userId: user.id, + name: user.name, + plan: user.plan as "free" | "pro", + preferredModel: user.preferredModel, + messageCount: user.messageCount, + }); + + const resolved = await systemPrompt.resolve({ + name: user.name, + plan: user.plan as string, + }); + chat.prompt.set(resolved); + + await prisma.chat.upsert({ + where: { id: chatId }, + create: { id: chatId, title: "New chat", userId: user.id, model: model ?? DEFAULT_MODEL }, + update: {}, + }); +} + +export const aiChatRaw = chat.customAgent({ + id: "ai-chat-raw", + run: async (payload: ChatTaskWirePayload, { signal: runSignal }) => { + let currentPayload = payload; + const clientData = payload.metadata as { userId: string; model?: string } | undefined; + + if (currentPayload.trigger === "preload") { + if (clientData) { + await initUserContext(clientData.userId, currentPayload.chatId, clientData.model); + } + + const result = await chat.messages.waitWithIdleTimeout({ + idleTimeoutInSeconds: payload.idleTimeoutInSeconds ?? 60, + timeout: "1h", + spanName: "waiting for first message", + }); + if (!result.ok) return; + currentPayload = result.output; + } + + const currentClientData = (currentPayload.metadata ?? clientData) as + | { userId: string; model?: string } + | undefined; + + if (!userContext.userId && currentClientData) { + await initUserContext( + currentClientData.userId, + currentPayload.chatId, + currentClientData.model + ); + } + + const stop = chat.createStopSignal(); + const conversation = new chat.MessageAccumulator({ + compaction: { + shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > COMPACT_AFTER_TOKENS, + summarize: async ({ messages: msgs }) => { + const resolved = await compactionPrompt.resolve({}); + return generateText({ + model: registryLanguageModel(resolved.model, "openai:gpt-4o-mini"), + ...resolved.toAISDKTelemetry(), + messages: [...msgs, { role: "user" as const, content: resolved.text }], + }).then((r) => r.text); + }, + compactUIMessages: ({ summary }) => [ + { + id: generateId(), + role: "assistant" as const, + parts: [{ type: "text" as const, text: `[Summary]\n\n${summary}` }], + }, + ], + }, + pendingMessages: { + shouldInject: () => true, + prepare: ({ messages }) => [ + { + role: "user" as const, + content: [ + { + type: "text" as const, + text: `[User sent ${messages.length} message(s) while you were working]:\n${messages + .map((m) => textFromFirstPart(m)) + .join("\n")}`, + }, + ], + }, + ], + }, + }); + + for (let turn = 0; turn < 100; turn++) { + stop.reset(); + + const messages = await conversation.addIncoming( + currentPayload.messages, + currentPayload.trigger, + turn + ); + + const turnClientData = (currentPayload.metadata ?? currentClientData) as + | { userId: string; model?: string } + | undefined; + + userContext.messageCount++; + if (turnClientData?.model) { + userContext.preferredModel = turnClientData.model; + } + + const modelOverride = turnClientData?.model ?? userContext.preferredModel ?? undefined; + const useReasoning = useExtendedThinking(modelOverride); + const combinedSignal = AbortSignal.any([runSignal, stop.signal]); + + const steeringSub = chat.messages.on(async (msg) => { + const lastMsg = msg.messages?.[msg.messages.length - 1]; + if (lastMsg) await conversation.steerAsync(lastMsg); + }); + + const result = streamText({ + ...chat.toStreamTextOptions({ registry }), + model: languageModelForChatTurn(modelOverride), + messages: messages, + tools: { + inspectEnvironment, + webFetch, + deepResearch, + }, + stopWhen: stepCountIs(10), + abortSignal: combinedSignal, + providerOptions: { + openai: { user: turnClientData?.userId }, + anthropic: { + metadata: { user_id: turnClientData?.userId }, + ...(useReasoning ? { thinking: { type: "enabled", budgetTokens: 10000 } } : {}), + }, + }, + prepareStep: conversation.prepareStep(), + }); + + let response: UIMessage | undefined; + try { + response = await chat.pipeAndCapture(result, { signal: combinedSignal }); + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + if (runSignal.aborted) break; + } else { + throw error; + } + } finally { + steeringSub.off(); + } + + if (response) { + if (stop.signal.aborted && !runSignal.aborted) { + await conversation.addResponse(chat.cleanupAbortedParts(response)); + } else { + await conversation.addResponse(response); + } + } + + if (runSignal.aborted) break; + + let turnUsage: LanguageModelUsage | undefined; + try { + turnUsage = await result.totalUsage; + } catch { + /* non-fatal */ + } + await conversation.compactIfNeeded(turnUsage, { + chatId: currentPayload.chatId, + turn, + }); + + await prisma.chat.update({ + where: { id: currentPayload.chatId }, + data: { messages: conversation.uiMessages as unknown as ChatMessagesForWrite }, + }); + + if (userContext.hasChanged()) { + await prisma.user.update({ + where: { id: userContext.userId }, + data: { + messageCount: userContext.messageCount, + preferredModel: userContext.preferredModel, + }, + }); + } + + await chat.writeTurnComplete(); + + const next = await chat.messages.waitWithIdleTimeout({ + idleTimeoutInSeconds: 60, + timeout: "1h", + spanName: "waiting for next message", + }); + if (!next.ok) break; + currentPayload = next.output; + } + + stop.cleanup(); + }, +}); + +export const aiChatSession = chat + .withClientData({ + schema: z.object({ userId: z.string(), model: z.string().optional() }), + }) + .customAgent({ + id: "ai-chat-session", + run: async (payload: ChatTaskWirePayload, { signal }) => { + const clientData = payload.metadata as { userId: string; model?: string } | undefined; + + if (clientData) { + await initUserContext(clientData.userId, payload.chatId, clientData.model); + } + + const session = chat.createSession(payload, { + signal, + idleTimeoutInSeconds: payload.idleTimeoutInSeconds ?? 60, + timeout: "1h", + compaction: { + shouldCompact: ({ totalTokens }) => (totalTokens ?? 0) > COMPACT_AFTER_TOKENS, + summarize: async ({ messages: msgs }) => { + const resolved = await compactionPrompt.resolve({}); + return generateText({ + model: registryLanguageModel(resolved.model, "openai:gpt-4o-mini"), + ...resolved.toAISDKTelemetry(), + messages: [...msgs, { role: "user" as const, content: resolved.text }], + }).then((r) => r.text); + }, + compactUIMessages: ({ uiMessages, summary }) => [ + { + id: generateId(), + role: "assistant" as const, + parts: [{ type: "text" as const, text: `[Conversation summary]\n\n${summary}` }], + }, + ...uiMessages.slice(-4), + ], + }, + pendingMessages: { + shouldInject: () => true, + }, + }); + + for await (const turn of session) { + const turnClientData = (turn.clientData ?? clientData) as + | { userId: string; model?: string } + | undefined; + + userContext.messageCount++; + if (turnClientData?.model) userContext.preferredModel = turnClientData.model; + + const modelOverride = turnClientData?.model ?? userContext.preferredModel ?? undefined; + const useReasoning = useExtendedThinking(modelOverride); + + const result = streamText({ + ...chat.toStreamTextOptions({ registry }), + model: languageModelForChatTurn(modelOverride), + messages: turn.messages, + tools: { + inspectEnvironment, + webFetch, + deepResearch, + }, + stopWhen: stepCountIs(10), + abortSignal: turn.signal, + providerOptions: { + openai: { user: turnClientData?.userId }, + anthropic: { + metadata: { user_id: turnClientData?.userId }, + ...(useReasoning ? { thinking: { type: "enabled", budgetTokens: 10000 } } : {}), + }, + }, + }); + + await turn.complete(result); + + await prisma.chat.update({ + where: { id: turn.chatId }, + data: { messages: turn.uiMessages as unknown as ChatMessagesForWrite }, + }); + + if (userContext.hasChanged()) { + await prisma.user.update({ + where: { id: userContext.userId }, + data: { + messageCount: userContext.messageCount, + preferredModel: userContext.preferredModel, + }, + }); + } + } + }, +}); +// #endregion + +// ============================================================================ +// Hydrated agent — backend is source of truth for message history +// ============================================================================ +// +// Demonstrates three features: +// +// 1. `hydrateMessages` — backend loads message history from the DB on every +// turn instead of trusting the frontend. Prevents fabricated history. +// +// 2. `actionSchema` + `onAction` — typed custom actions (undo, rollback) +// sent via transport.sendAction(). The agent modifies history via +// chat.history.*, then the LLM responds to the updated state. +// +// 3. `chat.history` — imperative mutations used inside onAction to +// implement undo (slice off last exchange) and rollback (truncate to +// a specific message). +// + +export const aiChatHydrated = chat + .withClientData({ + schema: z.object({ model: z.string().optional(), userId: z.string() }), + }) + .agent({ + id: "ai-chat-hydrated", + idleTimeoutInSeconds: 60, + + // Load message history from the database on every turn. + // The frontend's accumulated messages are ignored — the DB is the + // single source of truth. New user messages arrive in `incomingMessages` + // and are appended + persisted before returning. + hydrateMessages: async ({ chatId, trigger, incomingMessages }) => { + const record = await prisma.chat.findUnique({ where: { id: chatId } }); + const stored = (record?.messages as unknown as UIMessage[]) ?? []; + + if (trigger === "submit-message" && incomingMessages.length > 0) { + const newMsg = incomingMessages[incomingMessages.length - 1]!; + stored.push(newMsg); + await prisma.chat.update({ + where: { id: chatId }, + data: { messages: stored as unknown as ChatMessagesForWrite }, + }); + } + + return stored; + }, + + // Typed actions the frontend can send via transport.sendAction() + actionSchema: z.discriminatedUnion("type", [ + z.object({ type: z.literal("undo") }), + z.object({ type: z.literal("rollback"), targetMessageId: z.string() }), + z.object({ type: z.literal("remove"), messageId: z.string() }), + z.object({ + type: z.literal("replace"), + messageId: z.string(), + text: z.string(), + }), + ]), + + onAction: async ({ action }) => { + switch (action.type) { + case "undo": + // Remove the last user message + assistant response + chat.history.slice(0, -2); + break; + case "rollback": + // Keep messages up to and including the target + chat.history.rollbackTo(action.targetMessageId); + break; + case "remove": + chat.history.remove(action.messageId); + break; + case "replace": + // Build a new UIMessage with the updated text + chat.history.replace(action.messageId, { + id: action.messageId, + role: "user" as const, + parts: [{ type: "text" as const, text: action.text }], + }); + break; + } + }, + + onChatStart: async ({ chatId, runId, chatAccessToken, clientData, preloaded }) => { + if (preloaded) return; + await initUserContext(clientData.userId, chatId, clientData.model); + await prisma.chatSession.upsert({ + where: { id: chatId }, + create: { id: chatId, publicAccessToken: chatAccessToken }, + update: { publicAccessToken: chatAccessToken }, + }); + }, + + onPreload: async ({ chatId, runId, chatAccessToken, clientData }) => { + if (!clientData) return; + await initUserContext(clientData.userId, chatId, clientData.model); + await prisma.chatSession.upsert({ + where: { id: chatId }, + create: { id: chatId, publicAccessToken: chatAccessToken }, + update: { publicAccessToken: chatAccessToken }, + }); + }, + + onTurnComplete: async ({ chatId, uiMessages, runId, chatAccessToken, lastEventId }) => { + // See aiChat.onTurnComplete — atomic to avoid the resume-replay race. + await prisma.$transaction([ + prisma.chat.update({ + where: { id: chatId }, + data: { messages: uiMessages as unknown as ChatMessagesForWrite }, + }), + prisma.chatSession.upsert({ + where: { id: chatId }, + create: { id: chatId, publicAccessToken: chatAccessToken, lastEventId }, + update: { publicAccessToken: chatAccessToken, lastEventId }, + }), + ]); + }, + + run: async ({ messages, clientData, stopSignal }) => { + return streamText({ + ...chat.toStreamTextOptions(), + model: languageModelForChatTurn( + clientData?.model ?? userContext.preferredModel ?? undefined + ), + messages, + abortSignal: stopSignal, + }); + }, + }); + +// ============================================================================ +// Upgrade test agent — calls chat.requestUpgrade() after 3 turns +// ============================================================================ + +export const upgradeTestAgent = chat.agent({ + id: "upgrade-test", + idleTimeoutInSeconds: 60, + onTurnStart: async ({ turn, ctx }) => { + logger.info("Upgrade test turn", { turn, version: ctx.run.version }); + if (turn >= 3) { + logger.info("Requesting upgrade after 3 turns"); + chat.requestUpgrade(); + } + }, + run: async ({ messages, signal }) => { + return streamText({ + model: openai("gpt-4o-mini"), + system: + "You are a helpful test assistant. Keep responses short (1-2 sentences). " + + "Always mention what turn number you think you're on based on the conversation history.", + messages, + abortSignal: signal, + }); + }, +}); diff --git a/references/ai-chat/src/trigger/pr-review.ts b/references/ai-chat/src/trigger/pr-review.ts new file mode 100644 index 00000000000..ae69107f595 --- /dev/null +++ b/references/ai-chat/src/trigger/pr-review.ts @@ -0,0 +1,293 @@ +import { chat } from "@trigger.dev/sdk/ai"; +import { logger, prompts } from "@trigger.dev/sdk"; +import { + streamText, + generateObject, + stepCountIs, + createProviderRegistry, +} from "ai"; +import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { z } from "zod"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../../lib/generated/prisma/client"; +import { + parseGitHubUrl, + cloneRepo, + cleanupClone, + githubApi, +} from "@/lib/pr-review-helpers"; +import { + repo, + prReviewTools, + type PRReviewUiMessage, +} from "@/lib/pr-review-tools"; + +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); +const prisma = new PrismaClient({ adapter }); + +const registry = createProviderRegistry({ openai, anthropic }); + +// #region System prompt +const prReviewSystemPrompt = prompts.define({ + id: "pr-review-system", + model: "anthropic:claude-sonnet-4-6", + content: `You are an expert code reviewer with deep knowledge of software engineering best practices, security vulnerabilities, performance patterns, and clean code principles. + +## Your workflow +1. When the user asks to review a PR, ALWAYS use the fetchPR tool first to load the PR data. +2. Read the diff carefully. For any file where the diff is unclear, use readFile to see the full context. +3. When you spot a potential issue, USE the executeCode tool to verify your claim before stating it. Don't say "this might fail" — prove it. +4. When suggesting a fix, use executeCode to verify the fix works before presenting it. + +## Review format +Structure your review as: + +### Summary +One paragraph overview of the PR's purpose and scope. + +### Findings +For each issue, use severity markers: +- 🔴 **Bug**: Definite or highly likely bugs, data loss risks, security vulnerabilities +- 🟡 **Suggestion**: Improvements to readability, performance, maintainability +- 🟢 **Nitpick**: Style preferences, naming, minor improvements + +Format each finding as: +**[severity] filename:line — Brief title** +Description of the issue. Reference exact code from the diff. + +### Overall Assessment +Is this PR ready to merge, needs minor changes, or needs significant rework? + +## Rules +- Be specific. Always cite filenames and line numbers from the diff. +- Be constructive. Explain WHY something is a problem and suggest a fix. +- Don't flag intentional patterns as bugs — use readFile to check context first. +- Don't hallucinate line numbers. Use the diff hunks or readFile output. +- If the diff is truncated, tell the user and offer to read specific files. +- Verify non-obvious claims with executeCode before including them.`, +}); +// #endregion + +// #region Self-review prompt +const prSelfReviewPrompt = prompts.define({ + id: "pr-review-self-review", + model: "anthropic:claude-haiku-4-5", + content: `You are a code review quality checker. Analyze the reviewer's comments for accuracy. + +Focus on: +- False positive bugs (code flagged as buggy that is actually correct) +- Incorrect assumptions about the code's behavior +- Claims that weren't verified by running code +- Overstated severity levels + +Be concise. Only flag genuine issues.`, +}); +// #endregion + +// #region Shared init helper +async function initRepo(chatId: string, userId: string, githubUrl: string) { + // 1. Look up user and their GitHub token from the database + const user = await prisma.user.findUnique({ where: { id: userId } }); + const githubToken = user?.githubToken ?? null; + + // 2. Parse the GitHub URL and clone the repo + const { owner, repo: repoName } = parseGitHubUrl(githubUrl); + const cwd = `/tmp/pr-review-${chatId}`; + + await cloneRepo({ owner, repo: repoName, clonePath: cwd, token: githubToken }); + + // 3. Fetch open PRs from GitHub API + const prs = await githubApi< + Array<{ + number: number; + title: string; + user: { login: string }; + head: { ref: string }; + }> + >(`/repos/${owner}/${repoName}/pulls?state=open&per_page=20&sort=updated`, githubToken); + + const openPRs = prs.map((pr) => ({ + number: pr.number, + title: pr.title, + author: pr.user.login, + headBranch: pr.head.ref, + })); + + // 4. Initialize per-run state + repo.init({ + cwd, + owner, + repo: repoName, + githubToken, + openPRs, + activePR: null, + }); + + // 5. Resolve and set system prompt + const resolved = await prReviewSystemPrompt.resolve({}); + chat.prompt.set(resolved); + + logger.info("PR review state initialized", { + owner, + repo: repoName, + cwd, + openPRCount: openPRs.length, + hasToken: !!githubToken, + }); +} +// #endregion + +// #region Agent definition +export const prReviewChat = chat + .withUIMessage({ + streamOptions: { + sendReasoning: true, + onError: (error) => { + logger.error("PR review stream error", { error }); + return "Something went wrong during review. Please try again."; + }, + }, + }) + .withClientData({ + schema: z.object({ + userId: z.string(), + githubUrl: z.string().url(), + }), + }) + .agent({ + id: "pr-review", + idleTimeoutInSeconds: 10, + preloadIdleTimeoutInSeconds: 10, + chatAccessTokenTTL: "60m", + + // #region onPreload — clone repo + fetch PRs before first message + onPreload: async ({ chatId, clientData }) => { + if (!clientData) return; + await initRepo(chatId, clientData.userId, clientData.githubUrl); + }, + // #endregion + + // #region onChatStart — fallback init when not preloaded + onChatStart: async ({ chatId, clientData, preloaded }) => { + if (preloaded) return; + await initRepo(chatId, clientData.userId, clientData.githubUrl); + }, + // #endregion + + // #region onTurnComplete — self-review for false positives + onTurnComplete: async ({ messages }) => { + chat.defer( + (async () => { + const resolved = await prSelfReviewPrompt.resolve({}); + + const review = await generateObject({ + model: anthropic("claude-haiku-4-5-20251001"), + ...resolved.toAISDKTelemetry(), + system: resolved.text, + prompt: `Review the code reviewer's latest response for accuracy:\n\n${messages + .filter((m) => m.role === "user" || m.role === "assistant") + .slice(-4) + .map( + (m) => + `${m.role}: ${ + typeof m.content === "string" + ? m.content + : Array.isArray(m.content) + ? m.content + .filter((p: any) => p.type === "text") + .map((p: any) => p.text) + .join("") + : "" + }` + ) + .join("\n\n")}`, + schema: z.object({ + hasFalsePositives: z + .boolean() + .describe("Whether the review contains false positives"), + corrections: z.array( + z.object({ + originalClaim: z + .string() + .describe("The claim from the review"), + correction: z + .string() + .describe("What should be corrected"), + severity: z.enum([ + "false-positive-bug", + "overstated-severity", + "missing-context", + ]), + }) + ), + }), + }); + + if ( + review.object.hasFalsePositives && + review.object.corrections.length > 0 + ) { + const correctionText = review.object.corrections + .map( + (c) => + `- ${c.severity}: "${c.originalClaim}" → ${c.correction}` + ) + .join("\n"); + + chat.inject([ + { + role: "user" as const, + content: `[Self-review correction]\n\nYour previous review may contain inaccuracies:\n${correctionText}\n\nIncorporate these corrections naturally if the user asks follow-up questions.`, + }, + ]); + } + })() + ); + }, + // #endregion + + // #region onComplete — cleanup clone directory + onComplete: async () => { + await cleanupClone(repo.cwd); + }, + // #endregion + + // #region run — stream code review response + run: async ({ messages, stopSignal }) => { + // Inject open PR list as context so the agent knows what's available + const prListContext = + repo.openPRs.length > 0 + ? `Open PRs for ${repo.owner}/${repo.repo}:\n${repo.openPRs + .map( + (pr) => + ` #${pr.number} — ${pr.title} (by ${pr.author}, branch: ${pr.headBranch})` + ) + .join("\n")}` + : ""; + + return streamText({ + ...chat.toStreamTextOptions({ registry }), + model: anthropic("claude-sonnet-4-6"), + messages: prListContext + ? [ + { + role: "user" as const, + content: `[Context] ${prListContext}`, + }, + ...messages, + ] + : messages, + tools: prReviewTools, + stopWhen: stepCountIs(15), + abortSignal: stopSignal, + providerOptions: { + anthropic: { + thinking: { type: "enabled", budgetTokens: 10000 }, + }, + }, + }); + }, + // #endregion + }); +// #endregion diff --git a/references/ai-chat/src/trigger/skills/time-utils/SKILL.md b/references/ai-chat/src/trigger/skills/time-utils/SKILL.md new file mode 100644 index 00000000000..b9200574e77 --- /dev/null +++ b/references/ai-chat/src/trigger/skills/time-utils/SKILL.md @@ -0,0 +1,37 @@ +--- +name: time-utils +description: Compute and format dates/times in arbitrary timezones using a small set of bundled bash scripts. Use when the user asks about "what time is it", "current time in ", date math, or timezone conversions. +--- + +# Time utilities + +This skill bundles small bash scripts that shell out to `date` for timezone-aware answers without the model having to reason about offsets. + +## When to use + +- The user asks for the current time in a specific timezone (e.g. "what time is it in Tokyo?") +- The user wants a date formatted in a specific way +- The user needs a relative time (e.g. "what's the date 3 days from now?") + +## Scripts + +### `scripts/now.sh [TZ]` + +Prints the current time in the given IANA timezone (default `UTC`). Example: + +``` +bash scripts/now.sh America/Los_Angeles +``` + +### `scripts/add.sh DAYS [TZ]` + +Prints a date `DAYS` days from now in the given timezone. `DAYS` can be negative. Example: + +``` +bash scripts/add.sh 3 Europe/London +``` + +## Tips + +- IANA timezone names only (`America/New_York`, not `EST`). +- See `references/timezones.txt` for a short cheat-sheet of common zones. diff --git a/references/ai-chat/src/trigger/skills/time-utils/references/timezones.txt b/references/ai-chat/src/trigger/skills/time-utils/references/timezones.txt new file mode 100644 index 00000000000..dcbe5b31011 --- /dev/null +++ b/references/ai-chat/src/trigger/skills/time-utils/references/timezones.txt @@ -0,0 +1,30 @@ +Common IANA timezones: + +North America: + America/New_York (US Eastern) + America/Chicago (US Central) + America/Denver (US Mountain) + America/Los_Angeles (US Pacific) + America/Toronto + America/Mexico_City + +Europe: + Europe/London + Europe/Paris + Europe/Berlin + Europe/Amsterdam + Europe/Madrid + Europe/Dublin + +Asia & Pacific: + Asia/Tokyo + Asia/Shanghai + Asia/Singapore + Asia/Kolkata (India, UTC+5:30) + Australia/Sydney + Pacific/Auckland + +Others: + UTC + America/Sao_Paulo + Africa/Johannesburg diff --git a/references/ai-chat/src/trigger/skills/time-utils/scripts/add.sh b/references/ai-chat/src/trigger/skills/time-utils/scripts/add.sh new file mode 100755 index 00000000000..14470c0ac73 --- /dev/null +++ b/references/ai-chat/src/trigger/skills/time-utils/scripts/add.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +DAYS="${1:?days argument required}" +TZ="${2:-UTC}" +TZ="$TZ" date -d "${DAYS} days" '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null \ + || TZ="$TZ" date -v"${DAYS}d" '+%Y-%m-%d %H:%M:%S %Z' diff --git a/references/ai-chat/src/trigger/skills/time-utils/scripts/now.sh b/references/ai-chat/src/trigger/skills/time-utils/scripts/now.sh new file mode 100755 index 00000000000..836665f7921 --- /dev/null +++ b/references/ai-chat/src/trigger/skills/time-utils/scripts/now.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +TZ="${1:-UTC}" +TZ="$TZ" date -u '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || TZ="$TZ" date '+%Y-%m-%d %H:%M:%S %Z' diff --git a/references/ai-chat/src/trigger/stress-emit.ts b/references/ai-chat/src/trigger/stress-emit.ts new file mode 100644 index 00000000000..b9300c6ae25 --- /dev/null +++ b/references/ai-chat/src/trigger/stress-emit.ts @@ -0,0 +1,99 @@ +// Stress-test chat.agent. Emits a configurable number of `text-delta` +// chunks of a configurable size — no LLM call, no tokens spent. Lets us +// stress the dashboard's session detail view (rendered conversation + +// raw stream tabs) with deterministic load. +// +// Config is parsed from the last user message's text. Two formats: +// "1000 10" → chunkCount=1000, chunkSize=10 +// "1000 10 messages" → chunkCount messages of one delta each +// +// Defaults: 1000 chunks × 10 chars, single message. + +import { chat } from "@trigger.dev/sdk/ai"; +import { type UIMessage, simulateReadableStream, streamText } from "ai"; +import { MockLanguageModelV3 } from "ai/test"; +import type { LanguageModelV3StreamPart } from "@ai-sdk/provider"; + +type StressConfig = { + chunkCount: number; + chunkSize: number; + manyMessages: boolean; +}; + +function parseConfig(messages: UIMessage[]): StressConfig { + const lastUser = [...messages].reverse().find((m) => m.role === "user"); + const text = + lastUser?.parts?.[0]?.type === "text" ? lastUser.parts[0].text.trim() : ""; + const parts = text.split(/\s+/); + const chunkCount = Number(parts[0]); + const chunkSize = Number(parts[1]); + const manyMessages = parts[2] === "messages"; + return { + chunkCount: Number.isFinite(chunkCount) && chunkCount > 0 ? chunkCount : 1000, + chunkSize: Number.isFinite(chunkSize) && chunkSize > 0 ? chunkSize : 10, + manyMessages, + }; +} + +function buildModelStream(config: StressConfig): LanguageModelV3StreamPart[] { + const delta = "x".repeat(config.chunkSize); + // Each `text-start`/`text-end` pair maps to a separate assistant message + // in the AI SDK pipeline when `manyMessages` is set; without it, all + // deltas accumulate into a single message. + if (config.manyMessages) { + const stream: LanguageModelV3StreamPart[] = []; + for (let i = 0; i < config.chunkCount; i++) { + const id = `t${i}`; + stream.push({ type: "text-start", id }); + stream.push({ type: "text-delta", id, delta }); + stream.push({ type: "text-end", id }); + } + stream.push({ + type: "finish", + finishReason: { unified: "stop", raw: "stop" }, + usage: { + inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined }, + outputTokens: { + total: config.chunkCount, + text: config.chunkCount, + reasoning: undefined, + }, + }, + }); + return stream; + } + + const stream: LanguageModelV3StreamPart[] = [{ type: "text-start", id: "t1" }]; + for (let i = 0; i < config.chunkCount; i++) { + stream.push({ type: "text-delta", id: "t1", delta }); + } + stream.push({ type: "text-end", id: "t1" }); + stream.push({ + type: "finish", + finishReason: { unified: "stop", raw: "stop" }, + usage: { + inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined }, + outputTokens: { + total: config.chunkCount, + text: config.chunkCount, + reasoning: undefined, + }, + }, + }); + return stream; +} + +export const stressEmit = chat.agent({ + id: "stress-emit", + run: async ({ messages, signal }) => { + const config = parseConfig(messages); + const chunks = buildModelStream(config); + return streamText({ + model: new MockLanguageModelV3({ + doStream: async () => ({ stream: simulateReadableStream({ chunks }) }), + }), + messages, + abortSignal: signal, + }); + }, +}); diff --git a/references/ai-chat/src/trigger/test-chat.test.ts b/references/ai-chat/src/trigger/test-chat.test.ts new file mode 100644 index 00000000000..a0dbf45cba3 --- /dev/null +++ b/references/ai-chat/src/trigger/test-chat.test.ts @@ -0,0 +1,232 @@ +// Import the test harness FIRST so the resource catalog is installed +// before the agent module is loaded (which registers the task). +import { mockChatAgent } from "@trigger.dev/sdk/ai/test"; + +import { describe, expect, it } from "vitest"; +import { MockLanguageModelV3 } from "ai/test"; +import { simulateReadableStream, type UIMessage, type UIMessageChunk } from "ai"; +import type { LanguageModelV3StreamPart } from "@ai-sdk/provider"; +import { testChatAgent } from "./test-chat.js"; + +// ── Helpers ──────────────────────────────────────────────────────────── + +let msgCounter = 0; +function userMessage(text: string): UIMessage { + return { + id: `u-${++msgCounter}`, + role: "user", + parts: [{ type: "text", text }], + }; +} + +function assistantMessage(id: string, text: string): UIMessage { + return { + id, + role: "assistant", + parts: [{ type: "text", text }], + }; +} + +function modelWithText(text: string) { + const chunks: LanguageModelV3StreamPart[] = [ + { type: "text-start", id: "t1" }, + { type: "text-delta", id: "t1", delta: text }, + { type: "text-end", id: "t1" }, + { + type: "finish", + finishReason: { unified: "stop", raw: "stop" }, + usage: { + inputTokens: { total: 10, noCache: 10, cacheRead: undefined, cacheWrite: undefined }, + outputTokens: { total: 10, text: 10, reasoning: undefined }, + }, + }, + ]; + return new MockLanguageModelV3({ + doStream: async () => ({ stream: simulateReadableStream({ chunks }) }), + }); +} + +function collectText(chunks: UIMessageChunk[]): string { + return chunks + .filter((c) => c.type === "text-delta") + .map((c) => (c as { delta: string }).delta) + .join(""); +} + +// ── Tests ────────────────────────────────────────────────────────────── + +describe("testChatAgent", () => { + describe("basic flow", () => { + it("streams the model's response on a single turn", async () => { + const model = modelWithText("hello world"); + const harness = mockChatAgent(testChatAgent, { + chatId: "test-basic", + clientData: { model }, + }); + + try { + const turn = await harness.sendMessage(userMessage("hi there")); + expect(collectText(turn.chunks)).toBe("hello world"); + } finally { + await harness.close(); + } + }); + + it("handles multiple turns with the same harness", async () => { + const model = modelWithText("ok"); + const harness = mockChatAgent(testChatAgent, { + chatId: "test-multi", + clientData: { model }, + }); + + try { + await harness.sendMessage(userMessage("first")); + await harness.sendMessage(userMessage("second")); + + // Both turns should produce model output chunks + const turn1Chunks = harness.allChunks.filter((c) => c.type === "text-delta"); + expect(turn1Chunks.length).toBeGreaterThanOrEqual(2); + } finally { + await harness.close(); + } + }); + }); + + describe("onValidateMessages (content filter)", () => { + it("blocks messages containing the forbidden phrase", async () => { + const model = modelWithText("should never reach here"); + const harness = mockChatAgent(testChatAgent, { + chatId: "test-block", + clientData: { model }, + }); + + try { + const turn = await harness.sendMessage(userMessage("hello blocked-word here")); + + // The turn completes with an error chunk, not a text chunk + expect(collectText(turn.chunks)).toBe(""); + // The turn-complete wire chunk still arrives via rawChunks + expect(turn.rawChunks.some((c) => { + return typeof c === "object" && c !== null && + (c as { type?: string }).type === "trigger:turn-complete"; + })).toBe(true); + } finally { + await harness.close(); + } + }); + + it("allows clean messages through", async () => { + const model = modelWithText("alright"); + const harness = mockChatAgent(testChatAgent, { + chatId: "test-allow", + clientData: { model }, + }); + + try { + const turn = await harness.sendMessage(userMessage("hello there")); + expect(collectText(turn.chunks)).toBe("alright"); + } finally { + await harness.close(); + } + }); + }); + + describe("hydrateMessages", () => { + it("uses clientData.hydrated as the source of truth when provided", async () => { + const model = modelWithText("ok"); + // Pre-seed the hydrated set with a prior exchange + const hydrated: UIMessage[] = [ + { id: "h1", role: "user", parts: [{ type: "text", text: "prior question" }] }, + { id: "h2", role: "assistant", parts: [{ type: "text", text: "prior answer" }] }, + ]; + + const harness = mockChatAgent(testChatAgent, { + chatId: "test-hydrate", + clientData: { model, hydrated: [...hydrated, userMessage("follow up")] }, + }); + + try { + await harness.sendMessage(userMessage("follow up")); + + // Model should have been called with the hydrated context + expect(model.doStreamCalls).toHaveLength(1); + const modelMessages = model.doStreamCalls[0]!.prompt; + expect(modelMessages.length).toBeGreaterThanOrEqual(3); + } finally { + await harness.close(); + } + }); + }); + + describe("actions", () => { + it("handles the undo action via chat.history.slice", async () => { + const model = modelWithText("ok"); + const harness = mockChatAgent(testChatAgent, { + chatId: "test-undo", + clientData: { model }, + }); + + try { + await harness.sendMessage(userMessage("first")); + await harness.sendMessage(userMessage("second")); + + // Undo — should pop the last user+assistant exchange + const undoTurn = await harness.sendAction({ type: "undo" }); + + // The turn completes normally — undo + re-respond + expect(undoTurn.rawChunks.some((c) => { + return typeof c === "object" && c !== null && + (c as { type?: string }).type === "trigger:turn-complete"; + })).toBe(true); + } finally { + await harness.close(); + } + }); + + it("rejects invalid actions", async () => { + const model = modelWithText("ok"); + const harness = mockChatAgent(testChatAgent, { + chatId: "test-invalid", + clientData: { model }, + }); + + try { + await harness.sendMessage(userMessage("hi")); + + // Send an action that doesn't match the schema + const turn = await harness.sendAction({ type: "not-a-real-action" }); + + // An error chunk should be emitted instead of a clean turn + const errorChunks = turn.rawChunks.filter((c) => { + return typeof c === "object" && c !== null && + (c as { type?: string }).type === "error"; + }); + expect(errorChunks.length).toBeGreaterThan(0); + } finally { + await harness.close(); + } + }); + }); + + describe("model interaction", () => { + it("forwards the user message to the language model", async () => { + const model = modelWithText("echo"); + const harness = mockChatAgent(testChatAgent, { + chatId: "test-forward", + clientData: { model }, + }); + + try { + await harness.sendMessage(userMessage("the quick brown fox")); + + expect(model.doStreamCalls).toHaveLength(1); + const call = model.doStreamCalls[0]!; + // The model should have received a user message with our text + const userMessages = call.prompt.filter((m) => m.role === "user"); + expect(userMessages).toHaveLength(1); + } finally { + await harness.close(); + } + }); + }); +}); diff --git a/references/ai-chat/src/trigger/test-chat.ts b/references/ai-chat/src/trigger/test-chat.ts new file mode 100644 index 00000000000..c314c941d43 --- /dev/null +++ b/references/ai-chat/src/trigger/test-chat.ts @@ -0,0 +1,77 @@ +// A focused chat.agent built for offline testing. +// +// Real agents (aiChat, aiChatHydrated, etc.) depend on Prisma, the OpenAI +// provider registry, prompts, and the deployed environment. Those are +// integration concerns. For unit tests we want a minimal agent that +// exercises the turn loop + hooks without external dependencies. +// +// The model is pulled from clientData so tests can inject a MockLanguageModelV3. + +import { chat } from "@trigger.dev/sdk/ai"; +import { streamText, type LanguageModel, type UIMessage } from "ai"; +import { z } from "zod"; + +type TestClientData = { + /** The language model to use for this turn. Tests inject MockLanguageModelV3 here. */ + model: LanguageModel; + /** Optional pre-seeded messages returned by hydrateMessages. If absent, we use whatever the frontend sent. */ + hydrated?: UIMessage[]; +}; + +function textFromFirstPart(message: UIMessage): string { + const p = message.parts?.[0]; + return p?.type === "text" ? p.text : ""; +} + +export const testChatAgent = chat + .withClientData({ + schema: z.custom((v) => !!v && typeof v === "object" && "model" in (v as object)), + }) + .agent({ + id: "test-chat", + + // Validate messages: reject anything that looks like profanity. + // A realistic content-filter example. + onValidateMessages: async ({ messages }) => { + for (const m of messages) { + if (m.role === "user") { + const text = textFromFirstPart(m).toLowerCase(); + if (text.includes("blocked-word")) { + throw new Error("Message blocked by content filter"); + } + } + } + return messages; + }, + + // Hydrate from clientData if provided — simulates loading from DB. + hydrateMessages: async ({ clientData, incomingMessages }) => { + if (clientData?.hydrated) { + return clientData.hydrated; + } + return incomingMessages; + }, + + // Custom actions: undo and rollback. + actionSchema: z.discriminatedUnion("type", [ + z.object({ type: z.literal("undo") }), + z.object({ type: z.literal("rollback"), targetMessageId: z.string() }), + ]), + + onAction: async ({ action }) => { + if (action.type === "undo") { + // Slice off the last exchange (user + assistant) + chat.history.slice(0, -2); + } else if (action.type === "rollback") { + chat.history.rollbackTo(action.targetMessageId); + } + }, + + run: async ({ messages, clientData, signal }) => { + return streamText({ + model: clientData?.model ?? "openai/gpt-4o-mini", + messages, + abortSignal: signal, + }); + }, + }); diff --git a/references/ai-chat/trigger.config.ts b/references/ai-chat/trigger.config.ts new file mode 100644 index 00000000000..47592c760b1 --- /dev/null +++ b/references/ai-chat/trigger.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "@trigger.dev/sdk"; +import { prismaExtension } from "@trigger.dev/build/extensions/prisma"; + +export default defineConfig({ + project: process.env.TRIGGER_PROJECT_REF!, + dirs: ["./src/trigger"], + maxDuration: 3600, + runtime: "node-22", + processKeepAlive: { + enabled: true, + maxExecutionsPerProcess: 50, + }, + build: { + extensions: [ + prismaExtension({ + mode: "modern", + }), + ], + keepNames: false, + }, +}); diff --git a/references/ai-chat/tsconfig.json b/references/ai-chat/tsconfig.json new file mode 100644 index 00000000000..c1334095f87 --- /dev/null +++ b/references/ai-chat/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/references/ai-chat/vitest.config.ts b/references/ai-chat/vitest.config.ts new file mode 100644 index 00000000000..ecd0650cac7 --- /dev/null +++ b/references/ai-chat/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; +import path from "node:path"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + globals: true, + // The ai-chat reference app has Next.js + React code that we don't + // want vitest trying to transform for these pure-logic tests. Keep + // the env on `node` (default) and let users opt into jsdom per-file. + environment: "node", + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); diff --git a/references/hello-world/src/trigger/chatAgent.ts b/references/hello-world/src/trigger/chatAgent.ts new file mode 100644 index 00000000000..da0a2af077e --- /dev/null +++ b/references/hello-world/src/trigger/chatAgent.ts @@ -0,0 +1,56 @@ +import { chat } from "@trigger.dev/sdk/ai"; +import { prompts } from "@trigger.dev/sdk"; +import { streamText, createProviderRegistry } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { z } from "zod"; + +const registry = createProviderRegistry({ openai }); + +type RegistryModelId = Parameters[0]; + +const systemPrompt = prompts.define({ + id: "test-agent-system", + model: "openai:gpt-4o-mini" satisfies RegistryModelId, + config: { temperature: 0.7 }, + variables: z.object({ userId: z.string() }), + content: `You are a helpful AI assistant in the Trigger.dev playground. +The current user is {{userId}}. + +## Guidelines +- Be concise and friendly. Prefer short, direct answers. +- Use markdown formatting for code blocks and lists. +- If you don't know something, say so.`, +}); + +export const testAgent = chat + .withClientData({ + schema: z.object({ + userId: z.string().optional().default("anonymous"), + model: z.string().optional().default("openai:gpt-4o-mini"), + }), + }) + .onChatStart(async ({ clientData }) => { + const resolved = await systemPrompt.resolve({ + userId: clientData?.userId ?? "anonymous", + }); + chat.prompt.set(resolved); + }) + .agent({ + id: "test-agent", + run: async ({ messages, clientData, signal }) => { + // chat.toStreamTextOptions({ registry }) resolves the prompt's model via + // the registry and injects system prompt + telemetry automatically + const model = registry.languageModel(clientData?.model ? (clientData.model as RegistryModelId) : "openai:gpt-4o-mini") + + if (!model) { + throw new Error("Model not found"); + } + + return streamText({ + ...chat.toStreamTextOptions({ registry }), + model, + messages, + abortSignal: signal, + }); + }, + }); diff --git a/references/hello-world/src/trigger/triggerAndSubscribe.ts b/references/hello-world/src/trigger/triggerAndSubscribe.ts new file mode 100644 index 00000000000..347319159ce --- /dev/null +++ b/references/hello-world/src/trigger/triggerAndSubscribe.ts @@ -0,0 +1,276 @@ +import { logger, schemaTask, task, tasks } from "@trigger.dev/sdk"; +import { z } from "zod"; +import { setTimeout } from "timers/promises"; + +// A simple child task that does some work and returns a result +const childWork = schemaTask({ + id: "child-work", + schema: z.object({ + label: z.string(), + delayMs: z.number().default(1000), + shouldFail: z.boolean().default(false), + }), + run: async ({ label, delayMs, shouldFail }) => { + logger.info(`Child task "${label}" starting`, { delayMs, shouldFail }); + await setTimeout(delayMs); + if (shouldFail) { + throw new Error(`Child task "${label}" intentionally failed`); + } + logger.info(`Child task "${label}" done`); + return { label, completedAt: new Date().toISOString() }; + }, +}); + +// Test 1: Basic triggerAndSubscribe — single child task +export const testTriggerAndSubscribe = task({ + id: "test-trigger-and-subscribe", + run: async () => { + logger.info("Starting single triggerAndSubscribe test"); + + const result = await childWork + .triggerAndSubscribe({ label: "single", delayMs: 2000 }) + .unwrap(); + + logger.info("Got result", { result }); + return result; + }, +}); + +// Test 2: Parallel triggerAndSubscribe — multiple children concurrently +export const testParallelSubscribe = task({ + id: "test-parallel-subscribe", + run: async () => { + logger.info("Starting parallel triggerAndSubscribe test"); + + // This would fail with triggerAndWait due to preventMultipleWaits + const [result1, result2, result3] = await Promise.all([ + childWork.triggerAndSubscribe({ label: "parallel-1", delayMs: 2000 }).unwrap(), + childWork.triggerAndSubscribe({ label: "parallel-2", delayMs: 3000 }).unwrap(), + childWork.triggerAndSubscribe({ label: "parallel-3", delayMs: 1000 }).unwrap(), + ]); + + logger.info("All parallel tasks complete", { result1, result2, result3 }); + return { result1, result2, result3 }; + }, +}); + +// Test 3: Abort with cancelOnAbort: true (default) — child run gets cancelled +export const testAbortWithCancel = task({ + id: "test-abort-with-cancel", + run: async () => { + logger.info("Starting abort test (cancelOnAbort: true) — child should be cancelled"); + + const controller = new AbortController(); + + // Abort after 2 seconds + setTimeout(2000).then(() => { + logger.info("Firing abort signal"); + controller.abort(); + }); + + try { + const result = await childWork + .triggerAndSubscribe( + { label: "will-be-cancelled", delayMs: 10000 }, + { signal: controller.signal } + ) + .unwrap(); + + logger.error("Unexpected: task completed without being cancelled", { result }); + return { aborted: false, childCancelled: false, result }; + } catch (error) { + logger.info("Expected: subscription aborted and child cancelled", { + error: error instanceof Error ? error.message : String(error), + }); + return { aborted: true, childCancelled: true }; + } + }, +}); + +// Test 4: Abort with cancelOnAbort: false — child run keeps running +export const testAbortWithoutCancel = task({ + id: "test-abort-without-cancel", + run: async () => { + logger.info("Starting abort test (cancelOnAbort: false) — child should keep running"); + + const controller = new AbortController(); + + // Abort after 2 seconds + setTimeout(2000).then(() => { + logger.info("Firing abort signal"); + controller.abort(); + }); + + try { + const result = await childWork + .triggerAndSubscribe( + { label: "keeps-running", delayMs: 5000 }, + { signal: controller.signal, cancelOnAbort: false } + ) + .unwrap(); + + logger.error("Unexpected: task completed (subscription should have been aborted)", { + result, + }); + return { aborted: false, result }; + } catch (error) { + logger.info("Expected: subscription aborted but child still running", { + error: error instanceof Error ? error.message : String(error), + }); + // The child task should still complete on its own — we just stopped listening + return { aborted: true, childCancelled: false }; + } + }, +}); + +// Test 5: Abort signal already aborted before calling triggerAndSubscribe +export const testAbortAlreadyAborted = task({ + id: "test-abort-already-aborted", + run: async () => { + logger.info("Starting pre-aborted signal test"); + + const controller = new AbortController(); + controller.abort("pre-aborted"); + + try { + const result = await childWork + .triggerAndSubscribe( + { label: "should-not-run", delayMs: 1000 }, + { signal: controller.signal } + ) + .unwrap(); + + logger.error("Unexpected: task completed", { result }); + return { aborted: false }; + } catch (error) { + logger.info("Expected: immediately aborted", { + error: error instanceof Error ? error.message : String(error), + }); + return { aborted: true }; + } + }, +}); + +// Test 6: Standalone tasks.triggerAndSubscribe +export const testStandaloneSubscribe = task({ + id: "test-standalone-subscribe", + run: async () => { + logger.info("Starting standalone triggerAndSubscribe test"); + + const result = await tasks + .triggerAndSubscribe("child-work", { + label: "standalone", + delayMs: 1500, + }) + .unwrap(); + + logger.info("Got result", { result }); + return result; + }, +}); + +// Test 7: Result object without .unwrap() — success case +export const testResultSuccess = task({ + id: "test-result-success", + run: async () => { + const result = await childWork.triggerAndSubscribe({ + label: "result-success", + delayMs: 1000, + }); + + logger.info("Result object", { + ok: result.ok, + id: result.id, + taskIdentifier: result.taskIdentifier, + }); + + if (result.ok) { + logger.info("Success output", { output: result.output }); + return { ok: true, output: result.output, id: result.id }; + } else { + logger.error("Unexpected failure", { error: result.error }); + return { ok: false, error: String(result.error) }; + } + }, +}); + +// Test 8: Result object without .unwrap() — failure case +export const testResultFailure = task({ + id: "test-result-failure", + retry: { maxAttempts: 1 }, + run: async () => { + const result = await childWork.triggerAndSubscribe({ + label: "result-failure", + delayMs: 500, + shouldFail: true, + }); + + logger.info("Result object", { + ok: result.ok, + id: result.id, + taskIdentifier: result.taskIdentifier, + }); + + if (result.ok) { + logger.error("Unexpected success", { output: result.output }); + return { ok: true, output: result.output }; + } else { + logger.info("Expected failure", { error: String(result.error) }); + return { ok: false, error: String(result.error), id: result.id }; + } + }, +}); + +// Test 9: .unwrap() on a failed child — should throw SubtaskUnwrapError +export const testUnwrapFailure = task({ + id: "test-unwrap-failure", + retry: { maxAttempts: 1 }, + run: async () => { + try { + const output = await childWork + .triggerAndSubscribe({ + label: "unwrap-failure", + delayMs: 500, + shouldFail: true, + }) + .unwrap(); + + logger.error("Unexpected: unwrap succeeded", { output }); + return { threw: false, output }; + } catch (error) { + logger.info("Expected: unwrap threw", { + name: error instanceof Error ? error.name : "unknown", + message: error instanceof Error ? error.message : String(error), + }); + return { + threw: true, + errorName: error instanceof Error ? error.name : "unknown", + errorMessage: error instanceof Error ? error.message : String(error), + }; + } + }, +}); + +// Test 10: Parallel with mixed success/failure +export const testParallelMixed = task({ + id: "test-parallel-mixed", + retry: { maxAttempts: 1 }, + run: async () => { + const [success, failure] = await Promise.all([ + childWork.triggerAndSubscribe({ label: "mixed-success", delayMs: 1000 }), + childWork.triggerAndSubscribe({ label: "mixed-failure", delayMs: 500, shouldFail: true }), + ]); + + logger.info("Results", { + success: { ok: success.ok, output: success.ok ? success.output : null }, + failure: { ok: failure.ok, error: !failure.ok ? String(failure.error) : null }, + }); + + return { + successOk: success.ok, + successOutput: success.ok ? success.output : null, + failureOk: failure.ok, + failureError: !failure.ok ? String(failure.error) : null, + }; + }, +});