@promptev/client 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -1,6 +1,10 @@
1
- # Promptev JavaScript SDK
1
+ # Promptev JavaScript client
2
2
 
3
- The official JavaScript/TypeScript SDK for [Promptev.ai](https://promptev.ai) — run AI prompts and agents programmatically using your project API key.
3
+ The official JavaScript/TypeScript client for [Promptev](https://promptev.ai) — stream your AI **agents** and upload documents to their knowledge base, from anywhere that has `fetch`.
4
+
5
+ > In Promptev, an **agent** is your deployed AI assistant: instructions + model + tools (Jira, Slack, GitHub, your own APIs) + the documents it can retrieve, all configured in the app. This client runs those agents. Tools execute server-side, retrieval is governed server-side, and conversation memory is built in.
6
+
7
+ **Upgrading from 0.2.x? Read [Migrating from 0.2.x](#migrating-from-02x) first — 1.0 is a rewrite against a new API, and none of the 0.2 calls exist any more.**
4
8
 
5
9
  ## Installation
6
10
 
@@ -14,214 +18,244 @@ pnpm add @promptev/client
14
18
 
15
19
  Requires Node.js 18+ or any modern browser. Zero dependencies.
16
20
 
17
- ## Platform Support
18
-
19
- Works everywhere — backend, frontend, and edge runtimes. Built entirely on [Web Platform APIs](https://wintercg.org/) (`fetch`, `ReadableStream`, `AbortController`, `TextDecoder`) with no Node.js-specific modules.
21
+ ## Quick Start
20
22
 
21
- | Environment | Supported | Notes |
22
- |---|---|---|
23
- | **Node.js 18+** | ✅ | ESM and CommonJS |
24
- | **Bun** | ✅ | Native Web API support |
25
- | **Deno** | ✅ | Native Web API support |
26
- | **React / Next.js** | ✅ | Works with any bundler |
27
- | **Angular** | ✅ | Full TypeScript types included |
28
- | **Vue / Nuxt** | ✅ | Standard ESM import |
29
- | **Svelte / SvelteKit** | ✅ | Standard ESM import |
30
- | **Vanilla JS** | ✅ | No framework required |
31
- | **Cloudflare Workers** | ✅ | Edge runtime compatible |
32
- | **Vercel Edge Functions** | ✅ | Edge runtime compatible |
23
+ Mint an API key in Promptev under **Settings → API keys** of the project you want to reach. It looks like `fk_…`, it is shown exactly once, and it is bound to that one project.
33
24
 
34
- ```js
35
- // ESM (Node 18+, React, Next.js, Vue, Angular, Svelte, etc.)
25
+ ```ts
36
26
  import { PromptevClient } from "@promptev/client";
37
27
 
38
- // CommonJS (legacy Node.js projects)
39
- const { PromptevClient } = require("@promptev/client");
28
+ const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY! });
29
+
30
+ let conversationId: string | undefined;
31
+
32
+ for await (const event of client.chat("support-agent", "What is the refund policy?")) {
33
+ switch (event.type) {
34
+ case "conversation":
35
+ conversationId = event.id; // keep it to continue this thread
36
+ break;
37
+ case "token":
38
+ process.stdout.write(event.content); // the answer, one delta at a time
39
+ break;
40
+ case "approval_required":
41
+ console.log(`\n⏸ ${event.tool_name} is waiting for a human — ${event.reason}`);
42
+ break;
43
+ case "error":
44
+ throw new Error(event.message);
45
+ }
46
+ }
40
47
  ```
41
48
 
42
- ## Quick Start
43
-
44
- ```ts
45
- import { PromptevClient } from "@promptev/client";
46
-
47
- const client = new PromptevClient({ projectKey: "pv_sk_your_key_here" });
49
+ That is the whole conversational surface: one call that starts or continues a thread and streams everything the turn produces.
48
50
 
49
- // Run a prompt
50
- const result = await client.runPrompt("support-agent",
51
- "What is the refund policy?",
52
- { company: "Acme Corp" }
53
- );
54
- console.log(result);
51
+ ## Authentication
55
52
 
56
- // Chat with an AI agent
57
- const session = await client.startAgent("your-agent-id");
53
+ The key travels in a header, never in a URL:
58
54
 
59
- for await (const event of client.streamAgent(session.chatbotId, {
60
- sessionToken: session.sessionToken,
61
- query: "Summarize our Q4 sales report",
62
- })) {
63
- if (event.type === "done") console.log(event.output);
64
- }
55
+ ```
56
+ Authorization: Bearer fk_live_xxxxxxxx
65
57
  ```
66
58
 
67
- ## Prompts
59
+ Three things follow from that, and they are the reason 1.0 exists:
68
60
 
69
- Promptev prompts are versioned, server-managed templates. `runPrompt` compiles the template with your variables — and if the prompt has a model configured in Promptev, it also executes it against the LLM and returns the AI response directly.
61
+ - **The key never leaks into logs.** A key in a URL path ends up in access logs, proxy logs and `Referer` headers. A Bearer token does not.
62
+ - **The key carries its own retrieval identity.** What the agent may read when *you* call it is part of the credential (`retrieves_as`), not a parameter you send. A caller cannot widen its own reach.
63
+ - **The key is bound to one project.** An agent in a sibling project answers exactly the same `404` an unknown slug does — a key holder learns nothing about projects it was not given.
70
64
 
71
- ### Run a prompt with variables
65
+ Keys are minted by workspace admins, expire (at most a year), and can be revoked at any time. Keep them server-side.
66
+
67
+ ## `chat` — stream a turn
72
68
 
73
69
  ```ts
74
- const result = await client.runPrompt("support-agent",
75
- "How do I reset my password?",
76
- { company: "Acme Corp", tone: "professional" }
77
- );
70
+ client.chat(agentSlug, message, options?): AsyncGenerator<AgentEvent>
78
71
  ```
79
72
 
80
- ### Run a prompt without variables
73
+ | Parameter | Type | Description |
74
+ |---|---|---|
75
+ | `agentSlug` | `string` | The agent's slug, from its page in Promptev. It must be **published**, and it must be in the key's project — otherwise `NotFoundError`. |
76
+ | `message` | `string` | What to say. Required, non-empty after trimming, at most 20,000 characters. |
77
+ | `options.conversationId` | `string?` | Continue a thread. Take it from the first `conversation` event. |
78
+ | `options.onBehalfOf` | `object?` | Act for one of your end users — see [On behalf of your users](#on-behalf-of-your-users). |
79
+ | `options.signal` | `AbortSignal?` | Cancel the turn. |
80
+
81
+ Returns an async generator of [events](#events). It ends after `done`, or after `error`.
82
+
83
+ ### Multi-turn
81
84
 
82
85
  ```ts
83
- const result = await client.runPrompt("knowledge-base",
84
- "What is the refund policy?"
85
- );
86
+ let conversationId: string | undefined;
87
+
88
+ async function ask(question: string) {
89
+ let answer = "";
90
+ for await (const ev of client.chat("support-agent", question, { conversationId })) {
91
+ if (ev.type === "conversation") conversationId = ev.id;
92
+ if (ev.type === "token") answer += ev.content;
93
+ if (ev.type === "error") throw new Error(ev.message);
94
+ }
95
+ return answer;
96
+ }
97
+
98
+ console.log(await ask("Summarize our Q4 sales report"));
99
+ console.log(await ask("Compare that with Q3")); // remembers the first turn
86
100
  ```
87
101
 
88
- ### With a model configured (auto-execute)
102
+ Passing `conversationId` is worth doing for a second reason: a tool set to **Ask once** asks for approval on the first call in a conversation and not again for the rest of it, so a continued thread interrupts you far less often.
103
+
104
+ A thread belongs to the agent, to this API, **and** to the `onBehalfOf.subject` it was opened under. Keep the subject the same across a conversation's turns, and never reuse an id from the app, the portal or an embed — any mismatch answers `404`, not a fresh thread.
89
105
 
90
- If your prompt has a model and/or context packs attached in Promptev, `runPrompt` compiles the template, retrieves relevant context via RAG, sends it to the LLM, and returns the AI response:
106
+ ## Events
107
+
108
+ Every frame the server can send, typed. Narrow on `event.type`.
109
+
110
+ A frame from a **newer** server is passed through rather than thrown on, so an event added by a Promptev deploy can never break your integration — it simply matches none of your cases. If you want to log them, `import type { UnknownEvent }` and cast in your `default` branch.
111
+
112
+ | Event | Fields | What it means |
113
+ |---|---|---|
114
+ | `conversation` | `id` | Always first. The thread id — pass it back as `conversationId`. |
115
+ | `thinking` | `content` | One delta of the model's reasoning. Only from models that stream it. |
116
+ | `token` | `content` | One delta of the answer. Concatenate them all to get the reply. |
117
+ | `step` | `name`, `status`, `detail?` | A stage of the turn: reading attachments, retrieving, waiting on an emailed approval. `status` is `started` / `completed` / `failed`. |
118
+ | `sources` | `items[]` (`name`, `document_id`) | The documents the answer is grounded in. |
119
+ | `tool_call` | `id`, `name`, `args?` | The agent invoked a tool. |
120
+ | `tool_result` | `id`, `name`, `ok`, `summary?` | That tool answered. `id` matches the `tool_call`. |
121
+ | `approval_required` | `approval_id`, `tool_name`, `args?`, `reason?`, `channel?`, `expires_at?` | **The turn is paused, waiting for a human** — see below. |
122
+ | `artifact` | `id`, `name`, `mime`, `size` | A file the agent produced this turn. Fetch it from the conversation in Promptev. |
123
+ | `suggestions` | `questions[]` | 2–3 follow-ups the same person would plausibly ask next. |
124
+ | `usage` | `units`, `tokens?`, `tool_calls?` | What the turn cost, just before `done`. |
125
+ | `done` | — | The stream finished cleanly. |
126
+ | `error` | `message` | The turn failed mid-stream. |
127
+
128
+ ### `approval_required` — handle this one
129
+
130
+ A tool the agent wants to run can be gated on human approval. When that happens the turn **pauses and the stream stays open**: a person approves it in the Promptev app, by email, or in Slack, and the turn then continues on that same connection. You do not re-send anything. If nobody decides in time, the approval expires and the turn reports it as a failed `tool_result`.
131
+
132
+ An integration that ignores this event looks *hung* to its user for as long as the approval window lasts. Show it:
91
133
 
92
134
  ```ts
93
- const answer = await client.runPrompt("support-agent",
94
- "What is the refund policy?",
95
- { company: "Acme Corp" }
96
- );
97
- console.log(answer); // "Our refund policy allows returns within 30 days..."
135
+ for await (const ev of client.chat("ops-agent", "Refund invoice 4182")) {
136
+ if (ev.type === "approval_required") {
137
+ console.log(`Waiting on a human: ${ev.tool_name}`);
138
+ console.log(` why: ${ev.reason}`);
139
+ console.log(` args: ${JSON.stringify(ev.args)}`);
140
+ console.log(` decide: in Promptev (${ev.channel}), by ${ev.expires_at ?? "the policy deadline"}`);
141
+ }
142
+ if (ev.type === "tool_result" && ev.approval) {
143
+ console.log(`Approval ended: ${ev.approval}`); // rejected / timeout / expired
144
+ }
145
+ if (ev.type === "token") process.stdout.write(ev.content);
146
+ }
98
147
  ```
99
148
 
100
- ### Without a model (use with your own LLM)
149
+ `channel` is where a human can decide it — `"inline"` (the Promptev app) or `"slack"`. Approvals routed to an **approver by email** send no card at all; they show up as a `step` named `waiting for approval — <tool>`, which closes when the decision lands.
101
150
 
102
- If no model is configured, `runPrompt` returns the compiled template — use it with any LLM:
151
+ ## On behalf of your users
152
+
153
+ If you are putting Promptev behind your own UI, tell it which of your end users a call is for:
103
154
 
104
155
  ```ts
105
- import OpenAI from "openai";
156
+ for await (const ev of client.chat("support-agent", question, {
157
+ onBehalfOf: { subject: "ana@acme.com", groups: ["finance", "emea"] },
158
+ })) { /* … */ }
159
+ ```
106
160
 
107
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
108
- const openai = new OpenAI({ apiKey: "sk-..." });
161
+ - `subject` is recorded as the conversation's actor, for the audit trail.
162
+ - `groups` are **intersected** with the key's own grant. They can only ever narrow what the caller may retrieve, never widen it — so a compromised caller of yours still cannot read past what the key was given. (Because the assertion is unverified, the key's own per-user grants drop for that call; only an omitted `onBehalfOf` gets the key's full reach.)
109
163
 
110
- const systemPrompt = await client.runPrompt("support-agent",
111
- "How do I reset my password?",
112
- { company: "Acme Corp", tone: "professional" }
113
- );
164
+ ## `uploadDocument` — add to the knowledge base
114
165
 
115
- const response = await openai.chat.completions.create({
116
- model: "gpt-4",
117
- messages: [
118
- { role: "system", content: systemPrompt },
119
- { role: "user", content: "How do I reset my password?" },
120
- ],
121
- });
122
- console.log(response.choices[0].message.content);
166
+ ```ts
167
+ client.uploadDocument(files, options?): Promise<{ results: UploadResult[] }>
123
168
  ```
124
169
 
125
- ### Stream a prompt (with tools or real-time output)
126
-
127
- When a prompt has tools attached (Jira, Slack, GitHub, etc.) or you want real-time output, use `streamPrompt`. It returns SSE events — same format as agent streaming:
170
+ Files land in the project's **Uploads** source — the same corpus the app writes to, so what you upload is what that project's agents retrieve. The key names the project; there is nothing to address.
128
171
 
129
172
  ```ts
130
- for await (const event of client.streamPrompt("research-assistant",
131
- "Find all P1 bugs assigned to me",
132
- { project: "ACME" }
133
- )) {
134
- if (event.type === "thoughts") console.log(`Thinking: ${event.output}`);
135
- else if (event.type === "processing") console.log(`Running: ${event.output}`);
136
- else if (event.type === "done") console.log(event.output);
173
+ import { readFile } from "node:fs/promises";
174
+
175
+ const { results } = await client.uploadDocument([
176
+ { name: "handbook.pdf", content: await readFile("handbook.pdf") },
177
+ { name: "policies.md", content: "# Refunds\n30 days." },
178
+ ]);
179
+
180
+ for (const r of results) {
181
+ console.log(`${r.filename}: ${r.status}${r.error ? ` — ${r.error}` : ""} (${r.chunks} chunks)`);
137
182
  }
138
183
  ```
139
184
 
140
- > **When to use which:**
141
- > - `runPrompt()` — simple prompt execution, no tools, returns a string
142
- > - `streamPrompt()` — prompts with tools, RAG-heavy queries, or when you want real-time output
185
+ In a browser, pass `File` objects straight from an `<input type="file">`.
186
+
187
+ | Parameter | Type | Description |
188
+ |---|---|---|
189
+ | `files` | `File \| Blob \| { name, content, type? }`, or an array of them | `content` may be a `string`, `ArrayBuffer`, typed array, or `Blob`. |
190
+ | `options.mode` | `"hybrid" \| "graph"` | How it is indexed. `"graph"` needs a graph database and an extraction model configured for the workspace. |
191
+ | `options.signal` | `AbortSignal?` | Cancel the upload. |
192
+
193
+ Limits (enforced here and again on the server): **20 files per call**, **25 MB per file**. A larger set belongs in a connector source (Google Drive, SharePoint, Dropbox, …), configured in the app. Re-uploading identical bytes updates the existing document instead of duplicating it.
194
+
195
+ A per-file failure is *reported*, not thrown: every file gets a row in `results` with its own `status` and `error`. Only a whole-request failure (a bad key, no credits, a rejected file type) raises.
143
196
 
144
- ## Agents
197
+ > Ingestion is slow and the request does not return until every file is indexed. Raise `timeout` well above its 30-second default: `new PromptevClient({ apiKey, timeout: 300_000 })`.
145
198
 
146
- Promptev agents are deployed AI assistants with built-in memory, tools (Jira, Slack, GitHub, etc.), and RAG context packs. The SDK lets you start sessions and stream responses in real time.
199
+ ## `runOrchestration` — run a team of agents
147
200
 
148
- ### Start a session
201
+ An orchestration is a **team of agents** working one job. `chat` streams one agent's answer inline; an orchestration runs in a worker for as long as it takes, so `runOrchestration` hands back a run id straight away and `getRun` says what became of it.
149
202
 
150
203
  ```ts
151
- const session = await client.startAgent("your-agent-id", { visitor: "John" });
204
+ const started = await client.runOrchestration("month-end-close", {
205
+ period: "2026-03",
206
+ });
152
207
 
153
- console.log(session.sessionToken); // Use this for all subsequent messages
154
- console.log(session.name); // Agent display name
155
- console.log(session.memoryEnabled); // Whether agent retains conversation context
156
- ```
208
+ let run = await client.getRun(started.run_id);
209
+ while (run.status === "running" || run.status === "revising") {
210
+ await new Promise((r) => setTimeout(r, 2000));
211
+ run = await client.getRun(started.run_id);
212
+ }
157
213
 
158
- ### Stream a response
214
+ console.log(run.status); // "delivered"
215
+ console.log(run.output);
216
+ ```
159
217
 
160
- The agent responds via Server-Sent Events (SSE). Each event has a `type` and `output`:
218
+ The orchestration must be **published**, **switched on**, and in your key's project — one that is not published, or lives in a sibling project, throws the same `NotFoundError` an invented name does. One that exists and is published but is switched off says so plainly, so you can tell "turn it on" from "wrong name".
161
219
 
162
- | Event Type | Description |
163
- |---|---|
164
- | `thoughts` | Agent's internal reasoning |
165
- | `processing` | Tool execution status (e.g., "Searching Jira...") |
166
- | `approval_required` | Agent needs permission to run a tool |
167
- | `done` | Final response text |
168
- | `error` | Something went wrong |
220
+ The keys in the second argument are the orchestration's **own declared inputs** — the ones its Inputs step lists. A required one you leave out is refused before anything runs, with the field named:
169
221
 
170
222
  ```ts
171
- for await (const event of client.streamAgent(session.chatbotId, {
172
- sessionToken: session.sessionToken,
173
- query: "What are the open P1 bugs in our backlog?",
174
- })) {
175
- if (event.type === "thoughts") console.log(`Thinking: ${event.output}`);
176
- else if (event.type === "processing") console.log(`Running: ${event.output}`);
177
- else if (event.type === "done") console.log(`\n${event.output}`);
178
- else if (event.type === "error") console.log(`Error: ${event.output}`);
223
+ try {
224
+ await client.runOrchestration("month-end-close", {});
225
+ } catch (e) {
226
+ if (e instanceof ValidationError) console.log(e.field); // "period"
179
227
  }
180
228
  ```
181
229
 
182
- ### Multi-turn conversation
230
+ ### Sending files
183
231
 
184
- The session token maintains conversation context across messages:
232
+ An input declared `File(s)` takes a file, keyed by that input:
185
233
 
186
234
  ```ts
187
- const session = await client.startAgent("your-agent-id", { visitor: "Sarah" });
188
-
189
- // First message
190
- for await (const event of client.streamAgent(session.chatbotId, {
191
- sessionToken: session.sessionToken,
192
- query: "Summarize our Q4 sales report",
193
- })) {
194
- if (event.type === "done") console.log(event.output);
195
- }
196
-
197
- // Follow-up — agent remembers the previous context
198
- for await (const event of client.streamAgent(session.chatbotId, {
199
- sessionToken: session.sessionToken,
200
- query: "Compare that with Q3",
201
- })) {
202
- if (event.type === "done") console.log(event.output);
203
- }
235
+ await client.runOrchestration(
236
+ "contract-review",
237
+ { matter: "Acme renewal" },
238
+ { files: { contract: { name: "lease.pdf", content: bytes } } },
239
+ );
204
240
  ```
205
241
 
206
- ### Collect the final response only
242
+ Up to **five files per call**, 25 MB each. Each part is named after the input it fills, which is what lets one call fill two different file inputs. Files take the same shapes `uploadDocument` does — a `File`, a `Blob`, or `{ name, content }`.
207
243
 
208
- ```ts
209
- async function askAgent(client, session, query) {
210
- for await (const event of client.streamAgent(session.chatbotId, {
211
- sessionToken: session.sessionToken,
212
- query,
213
- })) {
214
- if (event.type === "done") return event.output;
215
- if (event.type === "error") throw new Error(event.output);
216
- }
217
- }
244
+ ### Retrying safely
218
245
 
219
- const answer = await askAgent(client, session, "What's our monthly churn rate?");
246
+ Starting a run twice runs the team twice. Pass an `idempotencyKey` and a retry returns the run you already started:
247
+
248
+ ```ts
249
+ await client.runOrchestration(
250
+ "month-end-close",
251
+ { period: "2026-03" },
252
+ { idempotencyKey: "close-2026-03" },
253
+ );
220
254
  ```
221
255
 
222
- ## Error Handling
256
+ Only your caller knows whether two calls are one event, which is why this is yours to send rather than something the client invents. Without one the start is **not** retried on a transient failure, because a retried start could fan the team out twice.
223
257
 
224
- The SDK raises typed errors for each failure scenario:
258
+ ## Error Handling
225
259
 
226
260
  ```ts
227
261
  import {
@@ -230,214 +264,297 @@ import {
230
264
  AuthenticationError,
231
265
  NotFoundError,
232
266
  RateLimitError,
267
+ InsufficientCreditsError,
233
268
  ServerError,
234
269
  NetworkError,
235
270
  } from "@promptev/client";
236
271
 
237
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
238
-
239
272
  try {
240
- const result = await client.runPrompt("my-prompt", "Hello", { name: "Ava" });
273
+ for await (const ev of client.chat("support-agent", "Hello")) {
274
+ if (ev.type === "token") process.stdout.write(ev.content);
275
+ }
241
276
  } catch (err) {
242
277
  if (err instanceof ValidationError) {
243
- // 400 — missing variables, bad input
244
- console.log(`Invalid request: ${err.message}`);
245
- } else if (err instanceof NotFoundError) {
246
- // 404 — prompt or project not found
247
- console.log(`Not found: ${err.message}`);
278
+ console.log(`Invalid request: ${err.message} (field: ${err.field})`);
248
279
  } else if (err instanceof AuthenticationError) {
249
- // 401/403 — invalid API key or agent not active
250
- console.log(`Auth error: ${err.message}`);
280
+ console.log(`Bad or revoked API key: ${err.message}`);
281
+ } else if (err instanceof NotFoundError) {
282
+ console.log(`No such published agent in this key's project: ${err.message}`);
283
+ } else if (err instanceof InsufficientCreditsError) {
284
+ console.log(`Out of credits — top up in Billing: ${err.message}`);
251
285
  } else if (err instanceof RateLimitError) {
252
- // 429 — API usage quota exceeded
253
- console.log(`Rate limited: ${err.message}`);
286
+ console.log(`Rate limited; retry in ${err.retryAfter ?? "a moment"}s`);
254
287
  } else if (err instanceof ServerError) {
255
- // 5xx — server error (after retries exhausted)
256
- console.log(`Server error: ${err.message}`);
288
+ console.log(`Server error (after retries): ${err.message}`);
257
289
  } else if (err instanceof NetworkError) {
258
- // Connection failed, timeout, DNS error
259
- console.log(`Network error: ${err.message}`);
290
+ console.log(`Could not reach Promptev: ${err.message}`);
260
291
  }
261
292
  }
262
293
  ```
263
294
 
264
- All errors extend `PromptevError` and include:
265
- - `err.statusCode` — HTTP status code (if applicable)
266
- - `err.responseText` — Raw response body (for debugging)
295
+ All errors extend `PromptevError` and carry:
296
+
297
+ - `err.statusCode` — the HTTP status, when there was one
298
+ - `err.field` — which input the server objected to, when it named one
299
+ - `err.responseText` — the raw body, for debugging
300
+
301
+ | Error | HTTP | When |
302
+ |---|---|---|
303
+ | `ValidationError` | 400, 422 | Empty or over-long message, bad `mode`, a rejected file type — and the same checks made client-side before a request goes out |
304
+ | `AuthenticationError` | 401, 403 | Missing, malformed, revoked or expired key |
305
+ | `InsufficientCreditsError` | 402 | The workspace is out of credits. Waiting does not fix it; a top-up does |
306
+ | `NotFoundError` | 404 | Unknown agent slug, an unpublished agent, or one outside the key's project — all three answer identically on purpose |
307
+ | `RateLimitError` | 429 | Too many requests. `err.retryAfter` is the server's own answer in seconds |
308
+ | `ServerError` | 5xx | The server failed, after retries |
309
+ | `NetworkError` | — | DNS, connection, abort, or the client timeout |
310
+ | `PromptevError` | any | Base class for all of the above |
311
+
312
+ **Retries.** `uploadDocument` retries 502/503/504 with exponential backoff (`maxRetries`, default 2), which is safe because a document's id is a hash of its bytes — the same upload twice is the same document. `chat` is deliberately **not** retried: re-sending a turn could run the agent, and its tools, a second time.
313
+
314
+ **A failed turn may also arrive as an `error` event** rather than an exception, when the stream had already started. Handle both.
315
+
316
+ ## What a call costs
317
+
318
+ Promptev is pay as you go and this API is metered. The rates below are the
319
+ managed ones; a workspace running on its own database and embedding provider
320
+ (BYO) is charged a flat **1** per operation instead, because those bills are
321
+ already yours.
322
+
323
+ | what happens | credits |
324
+ |---|---|
325
+ | any request to this API | **1** (`api_call`) |
326
+ | a chat turn | **1** (`chat_turn`) |
327
+ | each tool the agent calls | **1** |
328
+ | retrieval | **1** per knowledge base searched |
329
+ | retrieval in graph mode | **5** per knowledge base searched |
330
+ | a document you upload | **1** per page, slide or MB (minimum 1) |
331
+
332
+ So an ordinary `chat` costs **2** — one for the request, one for the turn — and
333
+ more if the agent calls tools or searches knowledge. The same conversation held
334
+ in the Promptev app costs one, because there is no request to the programmatic
335
+ door.
336
+
337
+ Two things worth knowing before they surprise you:
338
+
339
+ - **A refused request is still billed.** An unknown agent slug, a rate-limited
340
+ call and a rejected upload each consumed the same capacity a successful one
341
+ would, so each costs its `api_call` credit. Only the outcome differs.
342
+ - **A request this client rejects locally is never billed**, because it never
343
+ leaves your process. That is why the empty-message and upload-limit checks
344
+ exist here rather than being left to the server.
345
+
346
+ Model tokens are not billed by Promptev at all. Model calls go to your provider
347
+ on your own keys, and that bill is yours.
267
348
 
268
349
  ## Configuration
269
350
 
270
351
  ```ts
271
352
  const client = new PromptevClient({
272
- projectKey: "pv_sk_...", // Required — your project API key
273
- baseUrl: "https://api.promptev.ai", // Default — override for self-hosted
274
- timeout: 30000, // Default — request timeout in ms
275
- maxRetries: 2, // Default — retries for 502/503/504
276
- headers: { "X-Custom": "value" }, // Optional — extra HTTP headers
353
+ apiKey: "fk_...", // Required
354
+ baseUrl: "https://api.promptev.ai", // Default — override for self-hosted or during the cutover
355
+ timeout: 30000, // Default — ms to wait for the response
356
+ maxRetries: 2, // Default — retries for 502/503/504
357
+ headers: { "X-Custom": "value" }, // Optional — extra HTTP headers
277
358
  });
278
359
  ```
279
360
 
280
361
  | Parameter | Default | Description |
281
362
  |---|---|---|
282
- | `projectKey` | *required* | Your Promptev project API key |
283
- | `baseUrl` | `https://api.promptev.ai` | API base URL |
284
- | `timeout` | `30000` | Request timeout in milliseconds |
285
- | `maxRetries` | `2` | Automatic retries for transient server errors (502, 503, 504) |
363
+ | `apiKey` | *required* | Your Promptev API key (`fk_…`) |
364
+ | `baseUrl` | `https://api.promptev.ai` | API base URL. Self-hosted? Point it at your own host. |
365
+ | `timeout` | `30000` | Milliseconds to wait for the **response**. A `chat()` stream is not bounded by it — a turn that runs tools, or waits on an approval, may run for minutes. |
366
+ | `maxRetries` | `2` | Automatic retries for transient server errors (502, 503, 504) on non-streaming calls |
286
367
  | `headers` | `{}` | Additional HTTP headers |
287
368
 
288
- ## API Reference
369
+ ## Platform Support
289
370
 
290
- ### `PromptevClient`
371
+ Works everywhere — backend, frontend, and edge runtimes. Built entirely on [Web Platform APIs](https://wintercg.org/) (`fetch`, `ReadableStream`, `AbortController`, `TextDecoder`, `FormData`) with no Node.js-specific modules.
291
372
 
292
- | Method | Description | Returns |
373
+ | Environment | Supported | Notes |
293
374
  |---|---|---|
294
- | `runPrompt(promptKey, query, variables?)` | Compile and execute a prompt | `Promise<string>` |
295
- | `streamPrompt(promptKey, query, variables?)` | Stream prompt execution with tools | `AsyncGenerator<AgentEvent>` |
296
- | `startAgent(chatbotId, options?)` | Start agent session | `Promise<AgentSession>` |
297
- | `streamAgent(chatbotId, options)` | Stream agent response | `AsyncGenerator<AgentEvent>` |
375
+ | **Node.js 18+** | ✅ | ESM and CommonJS |
376
+ | **Bun** | ✅ | Native Web API support |
377
+ | **Deno** | ✅ | Native Web API support |
378
+ | **React / Next.js** | ✅ | Server-side — see the warning below |
379
+ | **Angular** | ✅ | Full TypeScript types included |
380
+ | **Vue / Nuxt** | ✅ | Standard ESM import |
381
+ | **Svelte / SvelteKit** | ✅ | Standard ESM import |
382
+ | **Cloudflare Workers** | ✅ | Edge runtime compatible |
383
+ | **Vercel Edge Functions** | ✅ | Edge runtime compatible |
298
384
 
299
- ### `AgentSession`
385
+ ```js
386
+ // ESM (Node 18+, React, Next.js, Vue, Angular, Svelte, etc.)
387
+ import { PromptevClient } from "@promptev/client";
300
388
 
301
- | Field | Type | Description |
302
- |---|---|---|
303
- | `sessionToken` | `string` | Token for subsequent stream calls |
304
- | `chatbotId` | `string` | Agent identifier |
305
- | `name` | `string` | Agent display name |
306
- | `memoryEnabled` | `boolean` | Whether agent retains conversation context |
307
- | `messages` | `array` | Previous messages (populated when resuming a session) |
389
+ // CommonJS (legacy Node.js projects)
390
+ const { PromptevClient } = require("@promptev/client");
391
+ ```
308
392
 
309
- ### `AgentEvent`
393
+ > **Do not ship your API key to a browser.** The key carries a retrieval identity: whoever holds it can read everything it was granted. Call Promptev from your own server, or put an agent in a page with Promptev's embed widget, which issues a short-lived per-visitor token instead.
310
394
 
311
- | Field | Type | Description |
312
- |---|---|---|
313
- | `type` | `string` | Event type: `thoughts`, `processing`, `done`, `error`, `approval_required` |
314
- | `output` | `string` | Event content text |
315
- | `raw` | `object` | Full parsed SSE event data |
395
+ ## Framework Examples
316
396
 
317
- ### Exceptions
397
+ ### Next.js route handler — stream to your own UI
318
398
 
319
- | Error | HTTP Status | When |
320
- |---|---|---|
321
- | `ValidationError` | 400 | Missing required variables, bad input |
322
- | `AuthenticationError` | 401, 403 | Invalid API key, agent not active |
323
- | `NotFoundError` | 404 | Project, prompt, or agent not found |
324
- | `RateLimitError` | 429 | API usage quota exceeded |
325
- | `ServerError` | 5xx | Server error (after retries exhausted) |
326
- | `NetworkError` | — | Connection failed, timeout, DNS error |
327
- | `PromptevError` | any | Base class for all above errors |
399
+ ```ts
400
+ // app/api/ask/route.ts
401
+ import { PromptevClient } from "@promptev/client";
328
402
 
329
- ## Framework Examples
403
+ const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY! });
404
+
405
+ export async function POST(req: Request) {
406
+ const { question, conversationId, user } = await req.json();
407
+
408
+ const stream = new ReadableStream({
409
+ async start(controller) {
410
+ const encoder = new TextEncoder();
411
+ try {
412
+ for await (const ev of client.chat("support-agent", question, {
413
+ conversationId,
414
+ onBehalfOf: { subject: user.email, groups: user.groups },
415
+ })) {
416
+ // Forward the events your UI cares about, as your own SSE stream.
417
+ if (ev.type === "token" || ev.type === "approval_required" || ev.type === "conversation") {
418
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`));
419
+ }
420
+ }
421
+ } catch (err) {
422
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "error", message: (err as Error).message })}\n\n`));
423
+ } finally {
424
+ controller.close();
425
+ }
426
+ },
427
+ });
428
+
429
+ return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
430
+ }
431
+ ```
330
432
 
331
- ### React
433
+ ### Express — collect the answer server-side
332
434
 
333
- ```tsx
334
- import { useState } from "react";
435
+ ```js
436
+ import express from "express";
335
437
  import { PromptevClient } from "@promptev/client";
336
438
 
337
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
338
-
339
- export function AskAI() {
340
- const [answer, setAnswer] = useState("");
341
- const [loading, setLoading] = useState(false);
439
+ const app = express();
440
+ const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY });
342
441
 
343
- async function handleAsk() {
344
- setLoading(true);
345
- try {
346
- const result = await client.runPrompt("support-agent", "What is your refund policy?");
347
- setAnswer(result);
348
- } finally {
349
- setLoading(false);
350
- }
442
+ app.post("/api/ask", express.json(), async (req, res) => {
443
+ let answer = "";
444
+ let conversationId = req.body.conversationId;
445
+ let pendingApproval = null;
446
+
447
+ for await (const ev of client.chat("support-agent", req.body.question, { conversationId })) {
448
+ if (ev.type === "conversation") conversationId = ev.id;
449
+ if (ev.type === "token") answer += ev.content;
450
+ if (ev.type === "approval_required") pendingApproval = ev; // tell the user!
451
+ if (ev.type === "error") return res.status(502).json({ error: ev.message });
351
452
  }
352
453
 
353
- return (
354
- <div>
355
- <button onClick={handleAsk} disabled={loading}>Ask AI</button>
356
- {answer && <p>{answer}</p>}
357
- </div>
358
- );
359
- }
454
+ res.json({ answer, conversationId, pendingApproval });
455
+ });
456
+
457
+ app.listen(3000);
360
458
  ```
361
459
 
362
- ### React — Streaming Agent Chat
460
+ ### Upload from a browser form (through your backend)
363
461
 
364
- ```tsx
365
- import { useState, useRef } from "react";
366
- import { PromptevClient } from "@promptev/client";
462
+ ```ts
463
+ // Your server route, holding the key:
464
+ const files = form.getAll("files") as File[];
465
+ const { results } = await client.uploadDocument(files, { mode: "hybrid" });
466
+ ```
367
467
 
368
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
468
+ ## Migrating from 0.2.x
369
469
 
370
- export function AgentChat() {
371
- const [messages, setMessages] = useState<string[]>([]);
372
- const [input, setInput] = useState("");
373
- const sessionRef = useRef(null);
470
+ **1.0 talks to a different backend.** 0.2.x targeted `POST /api/sdk/v1/agent/{project_key}/{agent_id}/…` on the previous product; Promptev's current API is `/api/v1`, which does not serve those paths at all. Every 0.2 call would 404. Upgrading is a rewrite of your call sites, not a version bump — 0.2.x still works if you have not moved yet, so migrate deliberately.
374
471
 
375
- async function startChat() {
376
- sessionRef.current = await client.startAgent("your-agent-id", { visitor: "user" });
377
- }
472
+ ### 1. Authentication: a Bearer key, not a key in the URL
473
+
474
+ ```diff
475
+ - const client = new PromptevClient({ projectKey: "pv_sk_..." });
476
+ + const client = new PromptevClient({ apiKey: "fk_..." });
477
+ ```
378
478
 
379
- async function sendMessage() {
380
- if (!sessionRef.current) await startChat();
479
+ Mint the new key in **Settings → API keys** of the project you want to reach. This is the change everything else follows from: the key is now a header, it is bound to one project, and it carries the retrieval identity the agent answers under. Your old `pv_sk_…` project key does not work against the new API.
381
480
 
382
- setMessages((prev) => [...prev, `You: ${input}`]);
383
- const query = input;
384
- setInput("");
481
+ ### 2. Base URL
385
482
 
386
- for await (const event of client.streamAgent(sessionRef.current.chatbotId, {
387
- sessionToken: sessionRef.current.sessionToken,
388
- query,
389
- })) {
390
- if (event.type === "done") {
391
- setMessages((prev) => [...prev, `Agent: ${event.output}`]);
392
- }
393
- }
394
- }
483
+ The default is `https://api.promptev.ai`. That host is being moved onto the new backend — until it is, pass `baseUrl: "https://console.promptev.ai"` explicitly. If you had hard-coded a `baseUrl`, remove it once the move is done, unless you are self-hosted.
395
484
 
396
- return (
397
- <div>
398
- {messages.map((msg, i) => <p key={i}>{msg}</p>)}
399
- <input value={input} onChange={(e) => setInput(e.target.value)} />
400
- <button onClick={sendMessage}>Send</button>
401
- </div>
402
- );
403
- }
485
+ ### 3. `startAgent` + `streamAgent` → `chat`
486
+
487
+ There is no session token any more. The key is the credential, and a `conversationId` is the thread.
488
+
489
+ ```diff
490
+ - const session = await client.startAgent(AGENT_ID, { visitor: "user_123" });
491
+ - for await (const ev of client.streamAgent(session.agentId, {
492
+ - sessionToken: session.sessionToken,
493
+ - query: "What is the refund policy?",
494
+ - })) {
495
+ - if (ev.type === "done") console.log(ev.output);
496
+ - }
497
+ + let conversationId: string | undefined;
498
+ + let answer = "";
499
+ + for await (const ev of client.chat("support-agent", "What is the refund policy?")) {
500
+ + if (ev.type === "conversation") conversationId = ev.id;
501
+ + if (ev.type === "token") answer += ev.content; // the answer arrives in deltas
502
+ + }
503
+ + console.log(answer);
404
504
  ```
405
505
 
406
- ### Node.js / Express API
506
+ Other differences to expect:
407
507
 
408
- ```js
409
- import express from "express";
410
- import { PromptevClient } from "@promptev/client";
508
+ - **Agents are addressed by slug**, not by a UUID from the Deploy tab.
509
+ - **Events changed shape.** There is no `event.output`; the answer arrives as `token` deltas that you concatenate, and `done` carries no text. See the [events table](#events). `thoughts` is now `thinking`; `processing` is now `step` / `tool_call` / `tool_result`.
510
+ - **`visitor` is now `onBehalfOf`**, and it does more than label a session — see [On behalf of your users](#on-behalf-of-your-users).
511
+ - **`variables` is gone.** Prompt variables were a property of the old product's templates; configure the agent's instructions in Promptev instead, or put the context in the message.
512
+ - **`approval_required` is real now.** The old SDK had no concept of a paused turn. Handle it.
411
513
 
412
- const app = express();
413
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
514
+ ### 4. `triggerAgent` → the project MCP server
414
515
 
415
- app.post("/api/ask", express.json(), async (req, res) => {
416
- const answer = await client.runPrompt("support-agent", req.body.question);
417
- res.json({ answer });
418
- });
516
+ `triggerAgent` — one blocking call that ran the agent and returned JSON — has **no equivalent in this client**. The 1.0 chat endpoint only streams, and faking a blocking call by draining the stream would silently swallow `approval_required`, leaving your integration hung with no way to know why.
419
517
 
420
- app.listen(3000);
518
+ Use Promptev's **project MCP server** instead, which is exactly the shape `trigger` was:
519
+
520
+ ```
521
+ POST https://api.promptev.ai/api/v1/mcp
522
+ Authorization: Bearer fk_... ← the same key
421
523
  ```
422
524
 
423
- ### Vanilla HTML
525
+ It exposes `list_agents` and `call_agent`; `call_agent(agent, message)` runs an agent and returns its answer in one call. A gated tool comes back as `status: "pending_approval"` with an id — decide it in Promptev and call again. Any MCP client works, including n8n's MCP Client node, Zapier, and Claude.
424
526
 
425
- ```html
426
- <script type="module">
427
- import { PromptevClient } from "https://cdn.jsdelivr.net/npm/@promptev/client/dist/esm/index.js";
527
+ If you would rather stay in this client, collect the answer yourself — and handle the pause:
428
528
 
429
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
529
+ ```ts
530
+ async function ask(slug: string, message: string, conversationId?: string) {
531
+ let answer = "";
532
+ const waiting: string[] = [];
533
+ for await (const ev of client.chat(slug, message, { conversationId })) {
534
+ if (ev.type === "conversation") conversationId = ev.id;
535
+ if (ev.type === "token") answer += ev.content;
536
+ if (ev.type === "approval_required") waiting.push(ev.tool_name);
537
+ if (ev.type === "error") throw new Error(ev.message);
538
+ }
539
+ return { answer, conversationId, waitedOn: waiting };
540
+ }
541
+ ```
430
542
 
431
- document.getElementById("ask-btn").addEventListener("click", async () => {
432
- const answer = await client.runPrompt("support-agent", "What is the refund policy?");
433
- document.getElementById("output").textContent = answer;
434
- });
435
- </script>
543
+ ### 5. What did not change
544
+
545
+ The typed error hierarchy, the retry/backoff configuration (`timeout`, `maxRetries`, `headers`), and the SSE parsing all behave as they did. `InsufficientCreditsError` (402) and `err.field` are new; nothing was removed.
546
+
547
+ ## Building from source
548
+
549
+ ```bash
550
+ npm install
551
+ npm run build # builds both ESM (dist/esm) and CommonJS (dist/cjs)
552
+ npm test # runs the offline test suite against the built output
436
553
  ```
437
554
 
438
555
  ## License
439
556
 
440
- This SDK is commercial software by [Promptev Inc](https://promptev.ai).
557
+ This client is commercial software by [Promptev Inc](https://promptev.ai).
441
558
 
442
559
  - Free tier use allowed
443
560
  - Production use requires an active subscription