@promptev/client 0.2.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,8 +1,10 @@
1
- # Promptev JavaScript SDK
1
+ # Promptev JavaScript client
2
2
 
3
- The official JavaScript/TypeScript SDK for [Promptev.ai](https://promptev.ai) — trigger and stream your AI **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
4
 
5
- > In Promptev, an **agent** is your deployed AI assistant: instructions + model + tools (Jira, Slack, GitHub, …) + context packs, all configured in the dashboard. This SDK runs those agents. Tools execute server-side, memory is built in.
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.**
6
8
 
7
9
  ## Installation
8
10
 
@@ -16,210 +18,245 @@ pnpm add @promptev/client
16
18
 
17
19
  Requires Node.js 18+ or any modern browser. Zero dependencies.
18
20
 
19
- ## Platform Support
21
+ ## Quick Start
20
22
 
21
- 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.
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.
22
24
 
23
- | Environment | Supported | Notes |
24
- |---|---|---|
25
- | **Node.js 18+** | ✅ | ESM and CommonJS |
26
- | **Bun** | ✅ | Native Web API support |
27
- | **Deno** | ✅ | Native Web API support |
28
- | **React / Next.js** | ✅ | Works with any bundler |
29
- | **Angular** | ✅ | Full TypeScript types included |
30
- | **Vue / Nuxt** | ✅ | Standard ESM import |
31
- | **Svelte / SvelteKit** | ✅ | Standard ESM import |
32
- | **Vanilla JS** | ✅ | No framework required |
33
- | **Cloudflare Workers** | ✅ | Edge runtime compatible |
34
- | **Vercel Edge Functions** | ✅ | Edge runtime compatible |
35
-
36
- ```js
37
- // ESM (Node 18+, React, Next.js, Vue, Angular, Svelte, etc.)
25
+ ```ts
38
26
  import { PromptevClient } from "@promptev/client";
39
27
 
40
- // CommonJS (legacy Node.js projects)
41
- 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
+ }
42
47
  ```
43
48
 
44
- ## Quick Start
49
+ That is the whole conversational surface: one call that starts or continues a thread and streams everything the turn produces.
45
50
 
46
- The fastest way to use an agent is `triggerAgent` — one blocking call that runs the agent and returns its final answer. Pass your agent's id (from the **Deploy** tab) and a message:
51
+ ## Authentication
47
52
 
48
- ```ts
49
- import { PromptevClient } from "@promptev/client";
53
+ The key travels in a header, never in a URL:
50
54
 
51
- const client = new PromptevClient({ projectKey: "pv_sk_your_key_here" });
52
-
53
- const { response, sessionId } = await client.triggerAgent(
54
- "your-agent-id",
55
- "What is the refund policy?",
56
- { variables: { company: "Acme Corp" } }
57
- );
58
- console.log(response);
59
-
60
- // Keep conversation memory by passing the same sessionId back:
61
- const followUp = await client.triggerAgent(
62
- "your-agent-id",
63
- "And for digital goods?",
64
- { sessionId }
65
- );
66
- console.log(followUp.response);
67
55
  ```
56
+ Authorization: Bearer fk_live_xxxxxxxx
57
+ ```
58
+
59
+ Three things follow from that, and they are the reason 1.0 exists:
68
60
 
69
- That's it. The agent's tools, context packs, and model all run server-side — you just get the answer.
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
- ## `triggerAgent` — run an agent in one call
65
+ Keys are minted by workspace admins, expire (at most a year), and can be revoked at any time. Keep them server-side.
72
66
 
73
- `triggerAgent(agentId, message, opts?)` creates **or** resumes a session **and** runs the agent, returning the final response as JSON.
67
+ ## `chat` — stream a turn
74
68
 
75
69
  ```ts
76
- const result = await client.triggerAgent("your-agent-id", "Summarize our Q4 sales report");
77
- console.log(result.response); // the agent's answer
78
- console.log(result.sessionId); // pass this back to continue the conversation
70
+ client.chat(agentSlug, message, options?): AsyncGenerator<AgentEvent>
79
71
  ```
80
72
 
81
73
  | Parameter | Type | Description |
82
74
  |---|---|---|
83
- | `agentId` | `string` | Your agent's id (from the Deploy tab) |
84
- | `message` | `string` | The user message to send to the agent |
85
- | `opts.sessionId` | `string?` | Pass the `sessionId` from a previous call to keep memory. Omit for a fresh, stateless run. |
86
- | `opts.variables` | `object?` | Fills the agent's `{{...}}` prompt variables |
87
- | `opts.visitor` | `string?` | Attribute the run to an end-user id (stored on the chat session for analytics/audit). Defaults to a generated `webhook_<id>`. Set it on the first call of a conversation; a resumed session keeps its original visitor. |
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`.
88
82
 
89
- Returns `{ response: string; sessionId: string }`.
83
+ ### Multi-turn
90
84
 
91
85
  ```ts
92
- // Attribute the run to a specific end-user:
93
- const { response } = await client.triggerAgent(
94
- "your-agent-id",
95
- "How do I reset my password?",
96
- { visitor: "user_12345" }
97
- );
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
98
100
  ```
99
101
 
100
- ### Stateful (with memory)
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.
101
103
 
102
- Pass the returned `sessionId` back on each follow-up to keep context:
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.
103
105
 
104
- ```ts
105
- let { response, sessionId } = await client.triggerAgent("your-agent-id", "Summarize Q4 sales");
106
- console.log(response);
106
+ ## Events
107
107
 
108
- ({ response } = await client.triggerAgent("your-agent-id", "Compare that with Q3", { sessionId }));
109
- console.log(response);
110
- ```
108
+ Every frame the server can send, typed. Narrow on `event.type`.
111
109
 
112
- ### Stateless (no memory)
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.
113
111
 
114
- Omit `sessionId` for an independent, one-shot run each time:
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:
115
133
 
116
134
  ```ts
117
- const { response } = await client.triggerAgent(
118
- "your-agent-id",
119
- "What is the refund policy?",
120
- { variables: { company: "Acme Corp" } }
121
- );
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
+ }
122
147
  ```
123
148
 
124
- ### With prompt variables
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.
125
150
 
126
- If your agent's instructions contain `{{...}}` placeholders, fill them via `variables`:
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:
127
154
 
128
155
  ```ts
129
- const { response } = await client.triggerAgent(
130
- "your-agent-id",
131
- "How do I reset my password?",
132
- { variables: { company: "Acme Corp", tone: "professional" } }
133
- );
156
+ for await (const ev of client.chat("support-agent", question, {
157
+ onBehalfOf: { subject: "ana@acme.com", groups: ["finance", "emea"] },
158
+ })) { /* … */ }
134
159
  ```
135
160
 
136
- ## Streaming an agent
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.)
163
+
164
+ ## `uploadDocument` — add to the knowledge base
137
165
 
138
- When you want real-time output — agent reasoning, tool-execution status, and the final answer as they happen — use the streaming flow: `startAgent` to open a session, then `streamAgent` to receive [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).
166
+ ```ts
167
+ client.uploadDocument(files, options?): Promise<{ results: UploadResult[] }>
168
+ ```
139
169
 
140
- ### Start a session
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.
141
171
 
142
172
  ```ts
143
- const session = await client.startAgent("your-agent-id", { visitor: "John" });
173
+ import { readFile } from "node:fs/promises";
144
174
 
145
- console.log(session.sessionToken); // use this for all subsequent stream calls
146
- console.log(session.agentId); // the agent id, echoed back
147
- console.log(session.name); // agent display name
148
- console.log(session.memoryEnabled); // whether the agent retains conversation context
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)`);
182
+ }
149
183
  ```
150
184
 
151
- ### Stream a response
185
+ In a browser, pass `File` objects straight from an `<input type="file">`.
152
186
 
153
- Each event has a `type` and `output`:
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. |
154
192
 
155
- | Event Type | Description |
156
- |---|---|
157
- | `thoughts` | Agent's internal reasoning |
158
- | `processing` | Tool execution status (e.g., "Searching Jira...") |
159
- | `approval_required` | Agent needs permission to run a tool |
160
- | `done` | Final response text |
161
- | `error` | Something went wrong |
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.
196
+
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 })`.
198
+
199
+ ## `runOrchestration` — run a team of agents
200
+
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.
162
202
 
163
203
  ```ts
164
- for await (const event of client.streamAgent(session.agentId, {
165
- sessionToken: session.sessionToken,
166
- query: "What are the open P1 bugs in our backlog?",
167
- })) {
168
- if (event.type === "thoughts") console.log(`Thinking: ${event.output}`);
169
- else if (event.type === "processing") console.log(`Running: ${event.output}`);
170
- else if (event.type === "done") console.log(`\n${event.output}`);
171
- else if (event.type === "error") console.log(`Error: ${event.output}`);
204
+ const started = await client.runOrchestration("month-end-close", {
205
+ period: "2026-03",
206
+ });
207
+
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);
172
212
  }
213
+
214
+ console.log(run.status); // "delivered"
215
+ console.log(run.output);
173
216
  ```
174
217
 
175
- ### Multi-turn conversation
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".
176
219
 
177
- The session token maintains conversation context across messages:
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:
178
221
 
179
222
  ```ts
180
- const session = await client.startAgent("your-agent-id", { visitor: "Sarah" });
181
-
182
- // First message
183
- for await (const event of client.streamAgent(session.agentId, {
184
- sessionToken: session.sessionToken,
185
- query: "Summarize our Q4 sales report",
186
- })) {
187
- if (event.type === "done") console.log(event.output);
188
- }
189
-
190
- // Follow-up — agent remembers the previous context
191
- for await (const event of client.streamAgent(session.agentId, {
192
- sessionToken: session.sessionToken,
193
- query: "Compare that with Q3",
194
- })) {
195
- if (event.type === "done") console.log(event.output);
223
+ try {
224
+ await client.runOrchestration("month-end-close", {});
225
+ } catch (e) {
226
+ if (e instanceof ValidationError) console.log(e.field); // "period"
196
227
  }
197
228
  ```
198
229
 
199
- ### Collect the final response only
230
+ ### Sending files
231
+
232
+ An input declared `File(s)` takes a file, keyed by that input:
200
233
 
201
234
  ```ts
202
- async function askAgent(client, session, query) {
203
- for await (const event of client.streamAgent(session.agentId, {
204
- sessionToken: session.sessionToken,
205
- query,
206
- })) {
207
- if (event.type === "done") return event.output;
208
- if (event.type === "error") throw new Error(event.output);
209
- }
210
- }
235
+ await client.runOrchestration(
236
+ "contract-review",
237
+ { matter: "Acme renewal" },
238
+ { files: { contract: { name: "lease.pdf", content: bytes } } },
239
+ );
240
+ ```
241
+
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 }`.
243
+
244
+ ### Retrying safely
245
+
246
+ Starting a run twice runs the team twice. Pass an `idempotencyKey` and a retry returns the run you already started:
211
247
 
212
- const answer = await askAgent(client, session, "What's our monthly churn rate?");
248
+ ```ts
249
+ await client.runOrchestration(
250
+ "month-end-close",
251
+ { period: "2026-03" },
252
+ { idempotencyKey: "close-2026-03" },
253
+ );
213
254
  ```
214
255
 
215
- > **When to use which:**
216
- > - `triggerAgent()` — the default. One call, blocking, returns the final answer. Best for APIs, jobs, and server-to-server use.
217
- > - `startAgent()` + `streamAgent()` — when you want to render reasoning and tool progress live (e.g., a chat UI).
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.
218
257
 
219
258
  ## Error Handling
220
259
 
221
- The SDK raises typed errors for each failure scenario:
222
-
223
260
  ```ts
224
261
  import {
225
262
  PromptevClient,
@@ -227,230 +264,297 @@ import {
227
264
  AuthenticationError,
228
265
  NotFoundError,
229
266
  RateLimitError,
267
+ InsufficientCreditsError,
230
268
  ServerError,
231
269
  NetworkError,
232
270
  } from "@promptev/client";
233
271
 
234
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
235
-
236
272
  try {
237
- const { response } = await client.triggerAgent("your-agent-id", "Hello");
238
- console.log(response);
273
+ for await (const ev of client.chat("support-agent", "Hello")) {
274
+ if (ev.type === "token") process.stdout.write(ev.content);
275
+ }
239
276
  } catch (err) {
240
277
  if (err instanceof ValidationError) {
241
- // 400 — missing variables, bad input
242
- console.log(`Invalid request: ${err.message}`);
243
- } else if (err instanceof NotFoundError) {
244
- // 404 — agent or project not found
245
- console.log(`Not found: ${err.message}`);
278
+ console.log(`Invalid request: ${err.message} (field: ${err.field})`);
246
279
  } else if (err instanceof AuthenticationError) {
247
- // 401/403 — invalid API key or agent not active
248
- 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}`);
249
285
  } else if (err instanceof RateLimitError) {
250
- // 429 — API usage quota exceeded
251
- console.log(`Rate limited: ${err.message}`);
286
+ console.log(`Rate limited; retry in ${err.retryAfter ?? "a moment"}s`);
252
287
  } else if (err instanceof ServerError) {
253
- // 5xx — server error (after retries exhausted)
254
- console.log(`Server error: ${err.message}`);
288
+ console.log(`Server error (after retries): ${err.message}`);
255
289
  } else if (err instanceof NetworkError) {
256
- // Connection failed, timeout, DNS error
257
- console.log(`Network error: ${err.message}`);
290
+ console.log(`Could not reach Promptev: ${err.message}`);
258
291
  }
259
292
  }
260
293
  ```
261
294
 
262
- All errors extend `PromptevError` and include:
263
- - `err.statusCode` — HTTP status code (if applicable)
264
- - `err.responseText` — Raw response body (for debugging)
295
+ All errors extend `PromptevError` and carry:
265
296
 
266
- | Error | HTTP Status | When |
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 |
267
302
  |---|---|---|
268
- | `ValidationError` | 400 | Missing required variables, bad input |
269
- | `AuthenticationError` | 401, 403 | Invalid API key, agent not active |
270
- | `NotFoundError` | 404 | Project or agent not found |
271
- | `RateLimitError` | 429 | API usage quota exceeded |
272
- | `ServerError` | 5xx | Server error (after retries exhausted) |
273
- | `NetworkError` | — | Connection failed, timeout, DNS error |
274
- | `PromptevError` | any | Base class for all above errors |
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.
275
348
 
276
349
  ## Configuration
277
350
 
278
351
  ```ts
279
352
  const client = new PromptevClient({
280
- projectKey: "pv_sk_...", // Required — your project API key
281
- baseUrl: "https://api.promptev.ai", // Default — override for self-hosted
282
- timeout: 30000, // Default — request timeout in ms
283
- maxRetries: 2, // Default — retries for 502/503/504
284
- 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
285
358
  });
286
359
  ```
287
360
 
288
361
  | Parameter | Default | Description |
289
362
  |---|---|---|
290
- | `projectKey` | *required* | Your Promptev project API key (`pv_sk_...`) |
291
- | `baseUrl` | `https://api.promptev.ai` | API base URL |
292
- | `timeout` | `30000` | Request timeout in milliseconds |
293
- | `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 |
294
367
  | `headers` | `{}` | Additional HTTP headers |
295
368
 
296
- ## API Reference
369
+ ## Platform Support
297
370
 
298
- ### `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.
299
372
 
300
- | Method | Description | Returns |
373
+ | Environment | Supported | Notes |
301
374
  |---|---|---|
302
- | `triggerAgent(agentId, message, opts?)` | Run an agent in one blocking call | `Promise<TriggerResult>` |
303
- | `startAgent(agentId, options?)` | Start a streaming agent session | `Promise<AgentSession>` |
304
- | `streamAgent(agentId, options)` | Stream an agent response (SSE) | `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 |
305
384
 
306
- ### `TriggerResult`
385
+ ```js
386
+ // ESM (Node 18+, React, Next.js, Vue, Angular, Svelte, etc.)
387
+ import { PromptevClient } from "@promptev/client";
307
388
 
308
- | Field | Type | Description |
309
- |---|---|---|
310
- | `response` | `string` | The agent's final answer |
311
- | `sessionId` | `string` | Pass back as `opts.sessionId` to continue the conversation |
389
+ // CommonJS (legacy Node.js projects)
390
+ const { PromptevClient } = require("@promptev/client");
391
+ ```
312
392
 
313
- ### `AgentSession`
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.
314
394
 
315
- | Field | Type | Description |
316
- |---|---|---|
317
- | `sessionToken` | `string` | Token for subsequent stream calls |
318
- | `agentId` | `string` | Agent identifier |
319
- | `name` | `string` | Agent display name |
320
- | `memoryEnabled` | `boolean` | Whether the agent retains conversation context |
321
- | `messages` | `array` | Previous messages (populated when resuming a session) |
395
+ ## Framework Examples
322
396
 
323
- ### `AgentEvent`
397
+ ### Next.js route handler — stream to your own UI
324
398
 
325
- | Field | Type | Description |
326
- |---|---|---|
327
- | `type` | `string` | Event type: `thoughts`, `processing`, `done`, `error`, `approval_required` |
328
- | `output` | `string` | Event content text |
329
- | `raw` | `object` | Full parsed SSE event data |
399
+ ```ts
400
+ // app/api/ask/route.ts
401
+ import { PromptevClient } from "@promptev/client";
330
402
 
331
- ## 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
+ ```
332
432
 
333
- ### React
433
+ ### Express — collect the answer server-side
334
434
 
335
- ```tsx
336
- import { useState } from "react";
435
+ ```js
436
+ import express from "express";
337
437
  import { PromptevClient } from "@promptev/client";
338
438
 
339
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
340
-
341
- export function AskAgent() {
342
- const [answer, setAnswer] = useState("");
343
- const [loading, setLoading] = useState(false);
439
+ const app = express();
440
+ const client = new PromptevClient({ apiKey: process.env.PROMPTEV_API_KEY });
344
441
 
345
- async function handleAsk() {
346
- setLoading(true);
347
- try {
348
- const { response } = await client.triggerAgent("your-agent-id", "What is your refund policy?");
349
- setAnswer(response);
350
- } finally {
351
- setLoading(false);
352
- }
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 });
353
452
  }
354
453
 
355
- return (
356
- <div>
357
- <button onClick={handleAsk} disabled={loading}>Ask Agent</button>
358
- {answer && <p>{answer}</p>}
359
- </div>
360
- );
361
- }
454
+ res.json({ answer, conversationId, pendingApproval });
455
+ });
456
+
457
+ app.listen(3000);
362
458
  ```
363
459
 
364
- ### React — Streaming Agent Chat
460
+ ### Upload from a browser form (through your backend)
365
461
 
366
- ```tsx
367
- import { useState, useRef } from "react";
368
- 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
+ ```
369
467
 
370
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
468
+ ## Migrating from 0.2.x
371
469
 
372
- export function AgentChat() {
373
- const [messages, setMessages] = useState<string[]>([]);
374
- const [input, setInput] = useState("");
375
- 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.
376
471
 
377
- async function startChat() {
378
- sessionRef.current = await client.startAgent("your-agent-id", { visitor: "user" });
379
- }
472
+ ### 1. Authentication: a Bearer key, not a key in the URL
380
473
 
381
- async function sendMessage() {
382
- if (!sessionRef.current) await startChat();
474
+ ```diff
475
+ - const client = new PromptevClient({ projectKey: "pv_sk_..." });
476
+ + const client = new PromptevClient({ apiKey: "fk_..." });
477
+ ```
383
478
 
384
- setMessages((prev) => [...prev, `You: ${input}`]);
385
- const query = input;
386
- setInput("");
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.
387
480
 
388
- for await (const event of client.streamAgent(sessionRef.current.agentId, {
389
- sessionToken: sessionRef.current.sessionToken,
390
- query,
391
- })) {
392
- if (event.type === "done") {
393
- setMessages((prev) => [...prev, `Agent: ${event.output}`]);
394
- }
395
- }
396
- }
481
+ ### 2. Base URL
397
482
 
398
- return (
399
- <div>
400
- {messages.map((msg, i) => <p key={i}>{msg}</p>)}
401
- <input value={input} onChange={(e) => setInput(e.target.value)} />
402
- <button onClick={sendMessage}>Send</button>
403
- </div>
404
- );
405
- }
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.
484
+
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);
406
504
  ```
407
505
 
408
- ### Node.js / Express API
506
+ Other differences to expect:
409
507
 
410
- ```js
411
- import express from "express";
412
- 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.
413
513
 
414
- const app = express();
415
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
514
+ ### 4. `triggerAgent` → the project MCP server
416
515
 
417
- app.post("/api/ask", express.json(), async (req, res) => {
418
- const { response, sessionId } = await client.triggerAgent(
419
- "your-agent-id",
420
- req.body.question,
421
- { sessionId: req.body.sessionId } // pass back to keep memory across turns
422
- );
423
- res.json({ answer: response, sessionId });
424
- });
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.
425
517
 
426
- app.listen(3000);
427
- ```
518
+ Use Promptev's **project MCP server** instead, which is exactly the shape `trigger` was:
428
519
 
429
- ### Vanilla HTML
520
+ ```
521
+ POST https://api.promptev.ai/api/v1/mcp
522
+ Authorization: Bearer fk_... ← the same key
523
+ ```
430
524
 
431
- ```html
432
- <script type="module">
433
- import { PromptevClient } from "https://cdn.jsdelivr.net/npm/@promptev/client/dist/esm/index.js";
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.
434
526
 
435
- const client = new PromptevClient({ projectKey: "pv_sk_..." });
527
+ If you would rather stay in this client, collect the answer yourself — and handle the pause:
436
528
 
437
- document.getElementById("ask-btn").addEventListener("click", async () => {
438
- const { response } = await client.triggerAgent("your-agent-id", "What is the refund policy?");
439
- document.getElementById("output").textContent = response;
440
- });
441
- </script>
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
+ }
442
541
  ```
443
542
 
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
+
444
547
  ## Building from source
445
548
 
446
549
  ```bash
447
550
  npm install
448
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
449
553
  ```
450
554
 
451
555
  ## License
452
556
 
453
- This SDK is commercial software by [Promptev Inc](https://promptev.ai).
557
+ This client is commercial software by [Promptev Inc](https://promptev.ai).
454
558
 
455
559
  - Free tier use allowed
456
560
  - Production use requires an active subscription